270 Commits

Author SHA1 Message Date
Dave Allie
567fa6e1e2 feat: Overhaul font format into CrossPoint font 2026-01-28 23:59:22 +11:00
Xuan-Son Nguyen
da4d3b5ea5 feat: add HalDisplay and HalGPIO (#522)
## Summary

Extracted some changes from
https://github.com/crosspoint-reader/crosspoint-reader/pull/500 to make
reviewing easier

This PR adds HAL (Hardware Abstraction Layer) for display and GPIO
components, making it easier to write a stub or an emulated
implementation of the hardware.

SD card HAL will be added via another PR, because it's a bit more
tricky.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
2026-01-28 04:50:15 +11:00
Eliz
172916afd4 feat: Display epub metadata on Recents (#511)
* **What is the goal of this PR?** Implement a metadata viewer for the
Recents screen
* **What changes are included?**

| Recents | Files |
| --- | --- |
| <img alt="image"
src="https://github.com/user-attachments/assets/e0f2d816-ddce-4a2e-bd4a-cd431d0e6532"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/3225cdce-d501-4175-bc92-73cb8bfe7a41"
/> |

For the Files screen, I have not made any changes on purpose. For the
Recents screen, we now display the Book title and author. If it is a
file with no epub metadata like txt or md, we display the file name
without the file extension.

---

Did you use AI tools to help write this code? _**< YES  >**_

Although I went trough all the code manually and made changes as well,
please be aware the majority of the code is AI generated.

---------

Co-authored-by: Eliz Kilic <elizk@google.com>
2026-01-28 04:25:42 +11:00
Dave Allie
ebcd813ff6 chore: Cut release 0.16.0 2026-01-28 04:08:04 +11:00
Dave Allie
712c566664 fix: Correctly render italics on image alt placeholders (#569)
## Summary

* Correctly render italics on image alt placeholders
  * Parser incorrectly handled depth of self-closing tags
  * Self-closing tags immediately call start and end tag

## Additional Context

* Previously, it would incorrectly make the whole chapter bold/italics,
or not italicised the image alt

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-28 03:33:36 +11:00
Boris Faure
5894ae5afe chore: .gitignore: add compile_commands.json & .cache (#568)
## Summary

* **What is the goal of this PR?**
Quality of Life

* **What changes are included?**
Add compile_commands.json & .cache to .gitignore .

Both are use by clangd that can help IDE support.

Run `pio run --target compiledb` to generate `compile_commands.json`.


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-28 03:32:33 +11:00
Dave Allie
8c1c80787a fix: Render keyboard entry over multiple lines (#567)
## Summary

* Render keyboard entry over multiple lines
  * Grows display areas based on input text
  * Shown on OPDS entry, but applies everywhere

## Additional Context

* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/554

| One line | Multi-line |
| --- | --- |
|
![IMG_5925](https://github.com/user-attachments/assets/28be00a8-7b90-4bf6-9ebf-4d4ad6642bc9)
|
![IMG_5926](https://github.com/user-attachments/assets/1c69a96f-d868-49a1-866c-546ca7b784ab)
|

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-28 02:43:04 +11:00
Arthur Tazhitdinov
140fcb9db5 fix: missing front layout in mapLabels() (#564)
## Summary

* adds missing front layout to mapLabels function
2026-01-28 02:09:05 +11:00
V
e0b6b9b28a refactor: Re-work for OTA feature (#509)
## Summary

Finally, I have received my device and got to chance to work on OTA. 
https://github.com/crosspoint-reader/crosspoint-reader/issues/176

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Existing OTA functionality is very buggy, many of times (I would say 8
out of 10) are end up with fail for me. When the time that it works it
is very slow and take ages. For others looks like end up with crash or
different issues.


* **What changes are included?**
To be honest, I'm not familiar with Arduino APIs of OTA process, but
looks like not good as much esp-idf itself. I always found Arduino APIs
very bulky for esp32. Wrappers and wrappers.

## Additional Context
Right now, OTA takes ~ 3min 10sec (of course depends on size of .bin
file). Can be tested with playing version info inside from
`platform.ini` file.

```
[crosspoint]
version = 0.14.0
```
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-01-28 01:30:27 +11:00
Daniel Chelling
83315b6179 perf: optimize large EPUB indexing from O(n^2) to O(n) (#458)
## Summary

Optimizes EPUB metadata indexing for large books (2000+ chapters) from
~30 minutes to ~50 seconds by replacing O(n²) algorithms with O(n log n)
hash-indexed lookups.

Fixes #134

## Problem

Three phases had O(n²) complexity due to nested loops:

| Phase | Operation | Before (2768 chapters) |
|-------|-----------|------------------------|
| OPF Pass | For each spine ref, scan all manifest items | ~25 min |
| TOC Pass | For each TOC entry, scan all spine items | ~5 min |
| buildBookBin | For each spine item, scan ZIP central directory | ~8.4
min |

Total: **~30+ minutes** for first-time indexing of large EPUBs.

## Solution

Replace linear scans with sorted hash indexes + binary search:

- **OPF Pass**: Build `{hash(id), len, offset}` index from manifest,
binary search for each spine ref
- **TOC Pass**: Build `{hash(href), len, spineIndex}` index from spine,
binary search for each TOC entry
- **buildBookBin**: New `ZipFile::fillUncompressedSizes()` API - single
ZIP central directory scan with batch hash matching

All indexes use FNV-1a hashing with length as secondary key to minimize
collisions. Indexes are freed immediately after each phase.

## Results

**Shadow Slave EPUB (2768 chapters):**

| Phase | Before | After | Speedup |
|-------|--------|-------|---------|
| OPF pass | ~25 min | 10.8 sec | ~140x |
| TOC pass | ~5 min | 4.7 sec | ~60x |
| buildBookBin | 506 sec | 34.6 sec | ~15x |
| **Total** | **~30+ min** | **~50 sec** | **~36x** |

**Normal EPUB (87 chapters):** 1.7 sec - no regression.

## Memory

Peak temporary memory during indexing:
- OPF index: ~33KB (2770 items × 12 bytes)
- TOC index: ~33KB (2768 items × 12 bytes)
- ZIP batch: ~44KB (targets + sizes arrays)

All indexes cleared immediately after each phase. No OOM risk on
ESP32-C3.

## Note on Threshold

All optimizations are gated by `LARGE_SPINE_THRESHOLD = 400` to preserve
existing behavior for small books. However, the algorithms work
correctly for any book size and are faster even for small books:

| Book Size | Old O(n²) | New O(n log n) | Improvement |
|-----------|-----------|----------------|-------------|
| 10 ch | 100 ops | 50 ops | 2x |
| 100 ch | 10K ops | 800 ops | 12x |
| 400 ch | 160K ops | 4K ops | 40x |

If preferred, the threshold could be removed to use the optimized path
universally.

## Testing

- [x] Shadow Slave (2768 chapters): 50s first-time indexing, loads and
navigates correctly
- [x] Normal book (87 chapters): 1.7s indexing, no regression
- [x] Build passes
- [x] clang-format passes

## Files Changed

- `lib/Epub/Epub/parsers/ContentOpfParser.h/.cpp` - OPF manifest index
- `lib/Epub/Epub/BookMetadataCache.h/.cpp` - TOC index + batch size
lookup
- `lib/ZipFile/ZipFile.h/.cpp` - New `fillUncompressedSizes()` API
- `lib/Epub/Epub.cpp` - Timing logs

<details>
<summary><b>Algorithm Details</b> (click to expand)</summary>

### Phase 1: OPF Pass - Manifest to Spine Lookup

**Problem**: Each `<itemref idref="ch001">` in spine must find matching
`<item id="ch001" href="...">` in manifest.

```
OLD: For each of 2768 spine refs, scan all 2770 manifest items
     = 7.6M string comparisons

NEW: While parsing manifest, build index:
     { hash("ch001"), len=5, file_offset=120 }
     
     Sort index, then binary search for each spine ref:
     2768 × log₂(2770) ≈ 2768 × 11 = 30K comparisons
```

### Phase 2: TOC Pass - TOC Entry to Spine Index Lookup

**Problem**: Each TOC entry with `href="chapter0001.xhtml"` must find
its spine index.

```
OLD: For each of 2768 TOC entries, scan all 2768 spine entries
     = 7.6M string comparisons

NEW: At beginTocPass(), read spine once and build index:
     { hash("OEBPS/chapter0001.xhtml"), len=25, spineIndex=0 }
     
     Sort index, binary search for each TOC entry:
     2768 × log₂(2768) ≈ 30K comparisons
     
     Clear index at endTocPass() to free memory.
```

### Phase 3: buildBookBin - ZIP Size Lookup

**Problem**: Need uncompressed file size for each spine item (for
reading progress). Sizes are in ZIP central directory.

```
OLD: For each of 2768 spine items, scan ZIP central directory (2773 entries)
     = 7.6M filename reads + string comparisons
     Time: 506 seconds

NEW: 
  Step 1: Build targets from spine
          { hash("OEBPS/chapter0001.xhtml"), len=25, index=0 }
          Sort by (hash, len)
  
  Step 2: Single pass through ZIP central directory
          For each entry:
            - Compute hash ON THE FLY (no string allocation)
            - Binary search targets
            - If match: sizes[target.index] = uncompressedSize
  
  Step 3: Use sizes array directly (O(1) per spine item)
  
  Total: 2773 entries × log₂(2768) ≈ 33K comparisons
  Time: 35 seconds
```

### Why Hash + Length?

Using 64-bit FNV-1a hash + string length as a composite key:
- Collision probability: ~1 in 2⁶⁴ × typical_path_lengths
- No string storage needed in index (just 12-16 bytes per entry)
- Integer comparisons are faster than string comparisons
- Verification on match handles the rare collision case

</details>

---

_AI-assisted development. All changes tested on hardware._
2026-01-28 01:29:15 +11:00
Lalo
8e0d2bece2 feat: Add Spanish hyphenation support (#558)
## Summary

* **What is the goal of this PR?** Add Spanish language hyphenation
support to improve text rendering for Spanish books.
* **What changes are included?**
- Added Spanish hyphenation trie (`hyph-es.trie.h`) generated from
Typst's hypher patterns
- Registered `spanishHyphenator` in `LanguageRegistry.cpp` for language
tag `es`
  - Added Spanish to the hyphenation evaluation test suite
  - Added Spanish test data file with 5000 test cases

## Additional Context

* **Test Results:** Spanish hyphenation achieves 99.02% F1 Score (97.72%
perfect matches out of 5000 test cases)
* **Compatibility:** Works automatically for EPUBs with
`<dc:language>es</dc:language>` (or es-ES, es-MX, etc.)
<img width="115" height="189" alt="imagen"
src="https://github.com/user-attachments/assets/9b92e7fc-b98d-48af-8d53-dfdc2e68abee"
/>


| Metric | Value |
|--------|-------|
| Perfect matches | 97.72% |
| Overall Precision | 99.33% |
| Overall Recall | 99.42% |
| Overall F1 Score | 99.38% |

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_

AI assisted with:
- Guiding and compile
- Preparing the PR description
2026-01-28 01:17:48 +11:00
Eliz
4848a77e1b feat: Add support to B&W filters to image covers (#476)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Implementation of a new feature in Display options as Image Filter
* **What changes are included?**
Black & White and Inverted Black & White options are added.

## Additional Context

Here are some examples:

| None | Contrast | Inverted |
| --- | --- | --- |
| <img alt="image"
src="https://github.com/user-attachments/assets/fe02dd9b-f647-41bd-8495-c262f73177c4"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/2d17747d-3ff6-48a9-b9b9-eb17cccf19cf"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/792dea50-f003-4634-83fe-77849ca49095"
/> |
| <img alt="image"
src="https://github.com/user-attachments/assets/28395b63-14f8-41e2-886b-8ddf3faeafc4"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/71a569c8-fc54-4647-ad4c-ec96e220cddb"
/> | <img alt="image"
src="https://github.com/user-attachments/assets/9139e32c-9175-433e-8372-45fa042d3dc9"
/> |



* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

I have also tried adding Color inversion, but could not see much
difference with that. It might be because my implementation was wrong.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** PARTIALLY **_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-28 00:21:59 +11:00
Arthur Tazhitdinov
49190cca6d feat(ux): page turning on button pressed if long-press chapter skip is disabled (#451)
## Summary

* If long-press chapter skip is disabled, turn pages on button pressed,
not released
* Makes page turning snappier
* Refactors MappedInputManager for readability

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY>**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 23:53:13 +11:00
Alex Faria
e9c2fe1c87 feat: Add status bar option "Full w/ Progress Bar" (#438)
## Summary

* **What is the goal of this PR?** This PR introduces a new "Status Bar"
mode that displays a visual progress bar at the bottom of the screen,
providing readers with a graphical indication of their position within
the book.
* **What changes are included?** 

* **Settings**: Updated SettingsActivity to expand the "Status Bar"
configuration with a new option: Full w/ Progress Bar.
* **EPUB Reader**: Modified EpubReaderActivity to calculate the global
book progress and render a progress bar at the bottom of the viewable
area when the new setting is active.
* **TXT Reader**: Modified TxtReaderActivity to implement similar
progress bar rendering logic based on the current page and total page
count.

## Additional Context

* The progress bar is rendered with a height of 4 pixels at the very
bottom of the screen (adjusted for margins).
* The feature reuses the existing renderStatusBar logic but
conditionally draws the bar instead of (or in addition to) other
elements depending on the specific implementation details in each
reader.
  * Renamed existing 'Full' mode to 'Full w/ Percentage'
  * Added new 'Full w/ Progress Bar' option

<img
src="https://github.com/user-attachments/assets/08c0dd49-c64c-4d4d-9fbb-f576c02d05d9"
width="500">


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 23:25:44 +11:00
Jonas Diemer
dd1741bf0b fix: Validate settings on read. (#492)
## Summary

Fixes #487 

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES **_

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 23:08:58 +11:00
Maeve Andrews
51c5c3c0aa fix: rotate origin in drawImage (#557)
## Summary

This was originally a comment in #499, but I'm making it its own PR,
because it doesn't depend on anything there and then I can base that PR
on this one.

Currently, `drawBitmap` is used for covers and sleep wallpaper, and
`drawImage` is used for the boot logo. `drawBitmap` goes row by row and
pixel by pixel, so it respects the renderer orientation. `drawImage`
just calls the `EInkDisplay`'s `drawImage`, which works in the eink
panel's native display orientation.

`drawImage` rotates the x,y coordinates where it's going to draw the
image, but doesn't account for the fact that the northwest corner in
portrait orientation becomes, the southwest corner of the image
rectangle in the native orientation. The boot and sleep activities
currently work around this by calculating the north*east* corner of
where the image should go, which becomes the northwest corner after
`rotateCoordinates`.

I think this wasn't really apparent because the CrossPoint logo is
rotationally symmetrical. The `EInkDisplay` `drawImage` always draws the
image in native orientation, but that looks the same for the "X" image.

If we rotate the origin coordinate in `GfxRenderer`'s `drawImage`, we
can use a much clearer northwest corner coordinate in the boot and sleep
activities. (And then, in #499, we can actually rotate the boot screen
to the user's preferred orientation).

This does *not* yet rotate the actual bits in the image; it's still
displayed in native orientation. This doesn't affect the
rotationally-symmetric logo, but if it's ever changed, we will probably
want to allocate a new `u8int[]` and transpose rows and columns if
necessary.

## Additional Context

I've created an additional branch on top of this to demonstrate by
replacing the logo with a non-rotationally-symmetrical image:

<img width="128" height="128" alt="Cat-in-a-pan-128-bw"
src="https://github.com/user-attachments/assets/d0b239bc-fe75-4ec8-bc02-9cf9436ca65f"
/>


https://github.com/crosspoint-reader/crosspoint-reader/compare/master...maeveynot:rotated-cat

(many thanks to https://notisrac.github.io/FileToCArray/)

As you can see, it is always drawn in native orientation, which makes it
sideways (turned clockwise) in portrait.

---

### AI Usage

No

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-27 22:59:41 +11:00
Dave Allie
5e24895f6d feat: Extract author from XTC/XTCH files (#563)
## Summary

* Extract author from XTC/XTCH files

## Additional Context

* Based on updated details in
https://gist.github.com/CrazyCoder/b125f26d6987c0620058249f59f1327d

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-27 22:56:51 +11:00
Егор Мартынов
e2ca0e94ca fix: add txt books to recent tab (#526)
Fixes #512

---

### AI Usage

 _**NO**_
2026-01-27 22:53:31 +11:00
Boris Faure
a4b9a43ca1 docs: add font generation commands to builtin font headers (#547)
## Summary

* **What is the goal of this PR?** Simple quality of life, ease
maintenance
* **What changes are included?**
Update fontconvert.py to include the command used to generate each font
file in the header comment, making it easier to regenerate fonts when
needed.

I plan on adding options to this scripts (kerning, and maybe ligatures),
thus knowing which command was used, even with already existing options
like `--additional-intervals`, is important.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 22:19:19 +11:00
Yaroslav
c73fca26f5 docs: Update README with supported languages for EPUB (#530)
## Summary

- Update README with the list of supported languages for EPUB files.
- Update USER_GUIDE with an extended list of supported and unsupported
languages.

## Additional Context

For weeks, I thought this firmware only supported English, because I
remember you saying that full language support would only be possible
after implementing proper font rendering. I also remember mentioning a
separate Korean fork, Vietnamese issues and so on.

All of this made it clear that this system doesn't support my languages.
I was surprised when I saw a Reddit post with a photo of a book in my
native language. Only then I did learn that such languages ​​are
supported. Therefore, mentioning the supported languages ​​would help
future buyers and new users.

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-01-27 22:14:32 +11:00
Carson Hicks
dfd7b615dc fix: Fix KOReader document md5 calculation for binary matching progress sync (#529)
## Summary

* **What is the goal of this PR?**
Resolve [KoSync progress does not sync between Crosspoint-reader and
KOReader
(Kindle)](https://github.com/crosspoint-reader/crosspoint-reader/issues/502)

* **What changes are included?**
KOReaderDocumentId::getOffset() - Update the value for the md5 offset
calculation to match KOReader.

## Additional Context

I've tested this with a couple of my ebooks and binary matching with
KOReader sync seems to be working fine now for both pushing and pulling
progress.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 22:14:07 +11:00
Jonas Diemer
aca6dceaa8 fix: Make sure img alt text is treated as separate text block (#497)
## Summary

Should address issues discussed in #168 and potentially fix #478.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-01-27 22:12:40 +11:00
GenesiaW
6ca75c4653 fix: goes to relative position when reader settings are changed (#486)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* Aims to fix Issue #220 

* **What changes are included?**
- Increased size of `progress.bin` such that total page count of current
section can be stored
- Comparison of total page count is done to determine if reader settings
were changed
- New position/page number is calculated using percentage calculated
from read progress

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 22:11:11 +11:00
Xuan-Son Nguyen
1b9c8ab545 fix: short-press power button to wakeup (#482)
## Summary

Fix https://github.com/crosspoint-reader/crosspoint-reader/issues/288

Based on my observation, it seems like the problem was that
`inputManager.isPressed(InputManager::BTN_POWER)` takes a bit of time
after waking up to report the correct value. I haven't tested this
behavior with a standalone ESP32C3, but if you know more about this,
feel free to comment.

However, if we just want short press, I think it's enough to check for
wake up source. If we plan to allow multiple buttons to wake up in the
future, may consider using ext1 / `esp_sleep_get_ext1_wakeup_status()`
to allow identify which pin triggered wake up.

Note that I'm not particularly experienced in esp32 developments, just
happen to have prior knowledge hacking esphome.

## Additional Context

N/A

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 22:07:37 +11:00
Vincent Politzer
bf6cf83577 fix: line break (#525)
## Summary

* Fixes #519 
* Refactors repeated code into new function:
`ChapterHtmlSlimParser::flushPartWordBuffer()`
    
## Additional Context 
  
* The `<br/>` tag is self closing and _in-line_, so the existing logic
for closing block tags does not get applied to `<br/>` tags.
* This PR adds the _in-line_ logic to:
* Flush the word preceding the `<br/>` tag from `partWordBuffer` to
`currentTextBlock` before calling `startNewTextBlock`
* **New function**: `ChapterHtmlSlimParser::flushPartWordBuffer()`
* **Purpose**: Consolidates the logic for flushing `partWordBuffer` to
`currentTextBlock`
* **Impact**: Simplifies `ChapterHtmlSlimParser::characterData(…)`,
`ChapterHtmlSlimParser::startElement(…)`, and
`ChapterHtmlSlimParser::endElement(…)` by integrating reused code into
single function

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 22:07:02 +11:00
Justin Mitchell
3a761b18af Refactors Calibre Wireless Device & Calibre Library (#404)
Our esp32 consistently dropped the last few packets of the TCP transfer
in the old implementation. Only about 1/5 transfers would complete. I've
refactored that entire system into an actual Calibre Device Plugin that
basically uses the exact same system as the web server's file transfer
protocol. I kept them separate so that we don't muddy up the existing
file transfer stuff even if it's basically the same at the end of the
day I didn't want to limit our ability to change it later.

I've also added basic auth to OPDS and renamed that feature to OPDS
Browser to just disassociate it from Calibre.

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 22:02:38 +11:00
Brendan O'Leary
13f0ebed96 UX improvment to Forget Network page (#484)
## Summary

On the Forget Network page

* Update the default option to be DON'T forget the network
* Make the options clearer ("Cancel" and "Forget network")
* Unify the button hints to match the rest of the UI

## Additional Context

Closes #427

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY
2026-01-27 21:26:17 +11:00
Arthur Tazhitdinov
0bc0baa966 feat: treat .md files as .txt (#498)
## Summary

* Quick fix for markdown reading - open them as txt files
2026-01-27 21:25:48 +11:00
Luke Stein
5d369df6be fix: Chapter Selection UI bugs when koreader sync is enabled, and clarify default kosync URL (#501)
## Summary

* Fixes #475
* Fixes #477
* Closes #428

## Additional Context

* Updates to
`src/activities/reader/EpubReaderChapterSelectionActivity.cpp` are
copied verbatim from #433 (thanks to @jonasdiemer)
* Update to `src/activities/settings/KOReaderSettingsActivity.cpp` per
discussion with @itsthisjustin at #428

Tested on my device with several books and koreader sync turned on and
off.

---

### AI Usage

Did you use AI tools to help write this code? _NO_
2026-01-27 21:25:25 +11:00
Sam Davis
b8ebcf5867 fix: remove decimal places from progress % (#507)
## Summary

Addresses
https://github.com/crosspoint-reader/crosspoint-reader/issues/504

- Reverts book progress % to showing as an integer instead of with a
decimal place
- This was changed to 1 decimal point of precision in
https://github.com/crosspoint-reader/crosspoint-reader/pull/232 from
what I can tell
- As this wasn't the primary intention of that PR, I'm assuming it was
left in accidentally

IMO having a decimal place of precision is too much for something as
vague as book completion percent. This de-clutters the status bar and
prevents extra updates as you change pages.

---

### AI Usage

YES
2026-01-27 21:24:39 +11:00
Boris Faure
e858ebbe88 feat: add new configuration for front buttons, more usable on landscape ccw (#460)
When reading on Landscape Counter ClockWise mode, the left/right button
appear inverted: the upper button (left) goes down and the lower button
(right) goes up.

Discussion: #449

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Add a new configuration for the front buttons: Back, Confirm, Right,
Left

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-27 21:15:42 +11:00
Jonas Diemer
9224bc3f8c fix: #348 fit cover artifacts 2 (#465)
Supersedes #358 and includes the bugfix from #351
2026-01-27 20:21:15 +11:00
Yaroslav
67a679ab41 fix: Add .vs folder to .gitignore (#466)
## Summary

* Adds Visual Studio project files folder to .gitignore

Otherwise:

<img width="425" height="193" alt="image"
src="https://github.com/user-attachments/assets/522d6eec-40c2-45d1-92af-01b0ec8f0dc1"
/>
2026-01-27 20:20:48 +11:00
Luke Stein
7a53342f9d fix: Allow line break after ellipsis and underscore (#425)
## Summary

* Add additional punctuation marks to the list of characters that can be
immediately followed by a line break even where there is no explicit
space

## Additional Context

* Huge appreciation to @osteotek for his amazing work on hyphenation.
Reading on the device is so much better now.
* I am getting bad line breaks when ellipses (…) are between words and
book file does not explicitly include some kind of breaking space.
* Per
[discussion](https://github.com/crosspoint-reader/crosspoint-reader/pull/305#issuecomment-3765411406),
several new characters are added in this PR to the `isExplicitHyphen`
list to allow line breaks immediately after them:

Character | Unicode | Usage | Why include it?
-- | -- | -- | --
Solidus (Slash) | U+002F | / | Essential for breaking URLs and "and/or"
constructs.
Backslash | U+005C | \ | Critical for technical text, file paths, and
coding documentation.
Underscore | U+005F | _ | Prevents "runaway" line lengths in usernames
or code snippets.
Middle Dot | U+00B7 | · | Acts as a semantic separator in dictionaries
or stylistic lists.
Ellipsis | U+2026 | … | Prevents justification failure when dialogue
lacks following spaces.
Midline Horizontal Ellipsis | U+22EF | ⋯ | Useful for mathematical
sequences and technical notation.


### Example:

This shows an example of what line breaking looks like *with* this PR.
Note the line break after "matter…" (which would not previously have
been allowed). It's particularly important here because the book
includes non-breaking spaces in "Mr. Aldrich" and "Mr. Rockefeller."


![IMG_2917](https://github.com/user-attachments/assets/8fa610a9-91dd-407f-8526-0019a8a7195f)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **PARTIALLY**
2026-01-27 20:18:09 +11:00
Dave Allie
3ce11f14ce chore: Cut release 0.15.0 2026-01-22 02:20:22 +11:00
KasyanDiGris
47ef92e8fd fix: OPDS browser OOM (#403)
## Summary

- Rewrite OpdsParser to stream parsing instead of full content
- Fix OOM due to big http xml response

Closes #385 

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-01-22 01:43:51 +11:00
Juan Biondi
e3d6e32609 docs: Add detailed webserver documentation (#446)
## More detailed documentation

* **What is the goal of this PR?** 
Add more information about the exposed webserver.
* **What changes are included?**
Detailed documentation for the webserver endpoints
(`./docs/webserver-endpoints.md`)
Adding a table of content so it is easier to navigate directly to the
section you're interested on (Almost all `.md` files or at least all
those relevant)

## Additional Context

Not sure if this would get accepted but I thought it might be useful for
those trying to create separate apps that would sync files to the
device. It was at least to me trying to upload files using python as
stated
[here](https://github.com/crosspoint-reader/crosspoint-reader/discussions/434#discussioncomment-15545349)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-01-22 00:29:39 +11:00
Logan Garbarini
d399afb53d feat: invalidate cache on web uploads and opds downloads and add Clear Cache action (#393)
## Summary

When uploading or downloading an updated ebook from SD/WebUI/OPDS with
same the filename the `.crosspoint` cache is not cleared. This can lead
to issues with the Table of Contents and hangs when switching between
chapters.

I encountered this issue in two places:
- When I need to do further ePub cleaning using Calibre after I load an
ePub and find that some of its formatting should be cleaned up. When I
reprocess the same book and want to place it back in the same location I
need a way to invalidate the cache.
- When syncing RSS feed generated epubs. I generate news ePubs with
filenames like `news-outlet.epub` and so every day when I fetch new news
the crosspoint cache needs to be cleared to load that file.

This change offers the following features:
- On web uploads, if the file already exists, the cache for that file is
cleared
- On OPDS downloads, if the file already exists, the cache for that file
is cleared
- There's now an action for `Clear Cache` in the Settings page which can
clear the cache for all books


Addresses
https://github.com/crosspoint-reader/crosspoint-reader/issues/281

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-22 00:06:07 +11:00
Arthur Tazhitdinov
838993259d fix: hard reset via RTS pin after flashing firmware (#437)
## Summary

* Disables going to sleep after uploading new firmware
* Makes developer experience easier

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 23:35:23 +11:00
Jonas Diemer
cc74039cab fix: Skip negative screen coordinates only after we read the bitmap row. (#431)
Otherwise, we don't crop properly.

Fixes #430 

### AI Usage

Did you use AI tools to help write this code? _**< NO >**_
2026-01-21 23:27:41 +11:00
Jonas Diemer
87d6c032a5 Reclaim space if we don't show battery Percentage (#352)
## Summary

Give space to the chapter title if we don't show battery percentage.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 12:09:48 +00:00
Dave Allie
c9b5462370 feat: Include superscripts and subscripts in fonts (#463)
## Summary

* Include superscripts and subscripts in fonts

## Additional Context

* Original change came from
https://github.com/crosspoint-reader/crosspoint-reader/pull/248

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No

---------

Co-authored-by: cor <cor@pruijs.dev>
2026-01-21 22:42:41 +11:00
Kenneth
e548bfc0e1 My Library: Tab bar w/ Recent Books + File Browser (#250)
# Summary

This PR introduces a reusable Tab Bar component and combines the Recent
Books and File Browser into a unified tabbed page called "My Library"
accessible from the Home screen.

## Features
### New Tab Bar Component
A flexible, reusable tab bar component added to `ScreenComponents` that
can be used throughout the application.

### New Scroll Indicator Component
A page position indicator for lists that span multiple pages.
**Features:**
- Up/down arrow indicators
- Current page fraction display (e.g., "1/3")
- Only renders when content spans multiple pages

### My Library Activity
A new unified view combining Recent Books and File Browser into a single
tabbed page.

**Tabs:**
- **Recent** - Shows recently opened books
- **Files** - Browse SD card directory structure

**Navigation:**
- Up/Down or Left/Right: Navigate through list items
- Left/Right (when first item selected): Switch between tabs
- Confirm: Open selected book or enter directory
- Back: Go up directory (Files tab) or return home
- Long press Back: Jump to root directory (Files tab)

**UI Elements:**
- Tab bar with selection indicator
- Scroll/page indicator on right side
- Side button hints (up/down arrows)
- Dynamic bottom button labels ("BACK" in subdirectories, "HOME" at
root)

## Tab Bar Usage
The tab bar component is designed to be reusable across different
activities. Here's how to use it:

### Basic Example
```cpp
#include "ScreenComponents.h"
void MyActivity::render() const {
  renderer.clearScreen();
  
  // Define tabs with labels and selection state
  std::vector<TabInfo> tabs = {
    {"Tab One", currentTab == 0},   // Selected when currentTab is 0
    {"Tab Two", currentTab == 1},   // Selected when currentTab is 1
    {"Tab Three", currentTab == 2}  // Selected when currentTab is 2
  };
  
  // Draw tab bar at Y position 15, returns height of the tab bar
  int tabBarHeight = ScreenComponents::drawTabBar(renderer, 15, tabs);
  
  // Position your content below the tab bar
  int contentStartY = 15 + tabBarHeight + 10; // Add some padding
  
  // Draw content based on selected tab
  if (currentTab == 0) {
    renderTabOneContent(contentStartY);
  } else if (currentTab == 1) {
    renderTabTwoContent(contentStartY);
  } else {
    renderTabThreeContent(contentStartY);
  }
  
  renderer.displayBuffer();
}
```
Video Demo: https://share.cleanshot.com/P6NBncFS

<img width="250"
src="https://github.com/user-attachments/assets/07de4418-968e-4a88-9b42-ac5f53d8a832"
/>
<img width="250"
src="https://github.com/user-attachments/assets/e40201ed-dcc8-4568-b008-cd2bf13ebb2a"
/>
<img width="250"
src="https://github.com/user-attachments/assets/73db269f-e629-4696-b8ca-0b8443451a05"
/>

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 11:38:38 +00:00
Daniel Poulter
73c30748d8 feat: adding categories to settings screen (#331)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
  file uploading.)

As we get more settings, I think it makes sense to do categories for
them. This just allows users to find the settings easier and navigate to
them.

* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

Co-authored-by: dpoulter <daniel@yoco.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 11:13:51 +00:00
GenesiaW
6d68466891 fix: truncate chapter names that are too long (#422)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- Implements a fix to truncate chapter names that exceeds display width
 
* **What changes are included?**
- Implements a fix to truncate chapter names that exceeds display width
## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).
- Prior to the fix, if the book contains multiple chapters with names
longer than the display width, there is a noticeable delay when
scrolling through the list of chapters.

Serial output of the issue:
```
[25673] [ACT] Entering activity: EpubReaderChapterSelection
[25693] [GFX] !! Outside range (485, 65) -> (65, -6)
[25693] [GFX] !! Outside range (486, 65) -> (65, -7)
[25693] [GFX] !! Outside range (487, 65) -> (65, -8)
[25693] [GFX] !! Outside range (488, 65) -> (65, -9)
[25693] [GFX] !! Outside range (485, 66) -> (66, -6)
[25693] [GFX] !! Outside range (486, 66) -> (66, -7)
[25694] [GFX] !! Outside range (487, 66) -> (66, -8)
[25694] [GFX] !! Outside range (484, 67) -> (67, -5)
[25694] [GFX] !! Outside range (485, 67) -> (67, -6)
[25694] [GFX] !! Outside range (486, 67) -> (67, -7)
[25694] [GFX] !! Outside range (483, 68) -> (68, -4)
[25694] [GFX] !! Outside range (484, 68) -> (68, -5)
[25694] [GFX] !! Outside range (485, 68) -> (68, -6)
``` 


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-19 13:01:51 +00:00
Arthur Tazhitdinov
8824c87490 feat: dict based Hyphenation (#305)
## Summary

* Adds (optional) Hyphenation for English, French, German, Russian
languages

## Additional Context

* Included hyphenation dictionaries add approximately 280kb to the flash
usage (German alone takes 200kb)
* Trie encoded dictionaries are adopted from hypher project
(https://github.com/typst/hypher)
* Soft hyphens (and other explicit hyphens) take precedence over
dict-based hyphenation. Overall, the hyphenation rules are quite
aggressive, as I believe it makes more sense on our smaller screen.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-19 12:56:26 +00:00
Maeve Andrews
5fef99c641 fix: render U+FFFD replacement character instead of ? (#366)
The current behavior of rendering `?` for an unknown Unicode character
can be hard to distinguish from a typo. Use the standard Unicode
"replacement character" instead, that's what it's designed for:

https://en.wikipedia.org/wiki/Specials_(Unicode_block)

I'm making this PR as a draft because I'm not sure I did everything that
was needed to change the character set covered by the fonts. Running
that script is in its own commit. If this is proper, I'll rebase/squash
into one commit and un-draft.

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-19 22:58:43 +11:00
Luke Stein
7a792a5384 fix: Invert colors on home screen cover overlay when recent book is selected (#390)
## Summary

* Fixes #388 

## Additional Context

* Tested on my own device
  * See images at #388 for what home screen looked like before.
* With this PR the home screen shows the following (selected and
unselected recent book × cover image rendered or not)


![Picsew_20260115153419](https://github.com/user-attachments/assets/44193f9d-76b7-4c77-b890-72b0dbae01c4)


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **YES**

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-19 22:57:39 +11:00
Justin Mitchell
f69cddf2cc Adds KOReader Sync support (#232)
## Summary

- Adds KOReader progress sync integration, allowing CrossPoint to sync
reading positions with other
KOReader-compatible devices
- Stores credentials securely with XOR obfuscation
- Uses KOReader's partial MD5 document hashing for cross-device book
matching
  - Syncs position via percentage with estimated XPath for compatibility

# Features
- Settings: KOReader Username, Password, and Authenticate options
- Sync from chapters menu: "Sync Progress" option appears when
credentials are configured
- Bidirectional sync: Can apply remote progress or upload local progress

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-19 11:55:35 +00:00
Nathan James
7185e5d287 feat: Change keyboard "caps" to "shift" & Wrap Keyboard (#377)
## Summary

* This PR solves issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/357 in the
first commit
* I then added an additional commit which means when you reach the end
of the keyboard, if you go 'beyond', you wrap back to the other side.
* This replaces existing behaviour, so if you would rather this be
removed, let me know and I'll just do the `caps` -> `shift` change

## Additional Context

### Screenshots for the new shift display

I thought it might not fit and need column size changes, but ended up
fitting fine, see screenshots showing this below:

<img width="573" height="366" alt="image"
src="https://github.com/user-attachments/assets/b8f6a4ec-94f5-4f5e-b9a6-06cc5f250ddb"
/>

<img width="570" height="308" alt="image"
src="https://github.com/user-attachments/assets/7d775518-4784-4120-a20a-a9dc67af8565"
/>


### Gif showing the wrap-around of the text



![IMG_7648](https://github.com/user-attachments/assets/7eec9066-e1cc-49a1-8b6b-a61556038d31)

---

### AI Usage

Did you use AI tools to help write this code? **PARTIALLY** - used to
double check the text wrapping had no edge-cases. (It did also suggest
rewriting the function, but I decided that was too big of a change for a
working part of the codebase, for now!)
2026-01-19 22:50:34 +11:00
Eunchurn Park
12940cc546 fix: XTC 1-bit thumb BMP polarity inversion (#373)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

- Fix inverted colors in Continue Reading cover image for 1-bit XTC
files

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

- Fix `grayValue = pixelBit ? 0 : 255` → `grayValue = pixelBit ? 255 :
0` in `lib/Xtc/Xtc.cpp`
- The thumb BMP generation had inverted polarity compared to cover BMP
generation
- bit=0 should be black, bit=1 should be white (matching the BMP palette
order)
- Update misleading comment about XTC polarity

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-01-19 22:41:48 +11:00
Luke Stein
21277e03eb docs: Update User Guide to reflect release 0.14.0 (#376) 2026-01-15 23:27:17 +11:00
Luke Stein
4eef2b5793 feat: Add MAC address display to WiFi Networks screen (#381)
## Summary

* Implements #380, allowing the user to see the device's MAC address in
order to register on wifi networks

## Additional Context

* Although @markatlnk suggested showing on the settings screen, I
implemented display at the bottom of the WiFi Networks selection screen
(accessed via "File Transfer" > "Join a Network") since I think it makes
more sense there.
* Tested on my own device


![IMG_2873](https://github.com/user-attachments/assets/b82a20dc-41a0-4b21-81f1-20876aa2c6b0)


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-15 23:26:39 +11:00
Nathan James
5a55fa1c6e fix: also apply longPressChapterSkip setting to xtc reader (#378)
## Summary

* This builds upon the helpful PR
https://github.com/crosspoint-reader/crosspoint-reader/pull/341, and
adds support for the setting to also apply to the XTC reader, which I
believed has just been missed and was not intentionally left out.
* XTC does not have chapter support yet, but it does skip 10 pages when
long-pressed, and so I think this is useful.

---

### AI Usage

Did you use AI tools to help write this code? No
2026-01-15 23:25:18 +11:00
Maeve Andrews
c98ba142e8 fix: draw button hints correctly if orientation is not portrait (#363)
~~Quick~~ fix for
https://github.com/daveallie/crosspoint-reader/issues/362

(this just applies to the chapter selection menu:)

~~If the orientation is portrait, hints as we know them make sense to
draw. If the orientation is inverted, we'd have to change the order of
the labels (along with everything's position), and if it's one of the
landscape choices, we'd have to render the text and buttons vertically.
All those other cases will be more complicated.~~

~~Punt on this for now by only rendering if portrait.~~

Update: this now draws the hints at the physical button position no
matter what the orientation is, by temporarily changing orientation to
portrait.

---------

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-15 23:23:36 +11:00
Jonas Diemer
c1c94c0112 Feature: Show img alt text (#168)
Let's start small by showing the ALT text of IMG. This is rudimentary,
but avoids those otherwise completely blank chapters.

I feel we will need this even when we can render images if that
rendering takes >1s - I would then prefer rendering optional and showing
the ALT text first.
2026-01-15 23:21:46 +11:00
Dave Allie
eb84bcee7c chore: Pin links2004/WebSockets version 2026-01-15 23:15:30 +11:00
efenner
d45f355e87 feat: Add EPUB table omitted placeholder (#372)
## Summary

* **What is the goal of this PR?**: Fix the bug I reported in
https://github.com/daveallie/crosspoint-reader/issues/292
* **What changes are included?**: Instead of silently dropping table
content in EPUBs., replace with an italicized '[Table omitted]' message
where tables appear.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY **_

---------

Co-authored-by: Evan Fenner <evan@evanfenner.com>
Co-authored-by: Warp <agent@warp.dev>
2026-01-15 23:14:59 +11:00
Dave Allie
56ec3dfb6d chore: Cut release 0.14.0 2026-01-15 01:01:47 +11:00
Maeve Andrews
e517945aaa Add a bullet to the beginning of any <li> (#368)
Currently there is no visual indication whatsoever if something is in a
list. An `<li>` is essentially just another paragraph.

As a partial remedy for this, add a bullet character to the beginning of
`<li>` text blocks so that the user can see that they're list items.
This is incomplete in that an `<ol>` should also have a counter so that
its list items can get numbers instead of bullets (right now I don't
think we track if we're in a `<ul>` or an `<ol>` at all), but it's
strictly better than the current situation.

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-14 23:23:03 +11:00
Maeve Andrews
489220832f Only indent paragraphs for justify/left-align (#367)
Currently, when Extra Paragraph Spacing is off, an em-space is added to
the beginning of each ParsedText even for blocks like headers that are
centered. This whitespace makes the centering slightly off. Change the
calculation here to only add the em-space for left/justified text.

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-14 23:21:48 +11:00
Dave Allie
3ee10b31ab Update OTA updater URL 2026-01-14 23:14:00 +11:00
swwilshub
a946c83a07 Turbocharge WiFi uploads with WebSocket + watchdog stability (#364)
## Summary

* **What is the goal of this PR?** Fix WiFi file transfer stability
issues (especially crashes during uploads) and improve upload speed via
WebSocket binary protocol. File transfers now don't really crash as
much, if they do it recovers and speed has gone form 50KB/s to 300+KB/s.

* **What changes are included?**
- **WebSocket upload support** - Adds WebSocket binary protocol for file
uploads, achieving faster speeds 335 KB/s vs HTTP multipart)
- **Watchdog stability fixes** - Adds `esp_task_wdt_reset()` calls
throughout upload path to prevent watchdog timeouts during:
    - File creation (FAT allocation can be slow)
    - SD card write operations
    - HTTP header parsing
    - WebSocket chunk processing
- **4KB write buffering** - Batches SD card writes to reduce I/O
overhead
- **WiFi health monitoring** - Detects WiFi disconnection in STA mode
and exits gracefully
- **Improved handleClient loop** - 500 iterations with periodic watchdog
resets and button checks for responsiveness
- **Progress bar improvements** - Fixed jumping/inaccurate progress by
capping local progress at 95% until server confirms completion
- **Exit button responsiveness** - Button now checked inside the
handleClient loop every 64 iterations
- **Reduced exit delays** - Decreased shutdown delays from ~850ms to
~140ms

**Files changed:**
- `platformio.ini` - Added WebSockets library dependency
- `CrossPointWebServer.cpp/h` - WebSocket server, upload buffering,
watchdog resets
- `CrossPointWebServerActivity.cpp` - WiFi monitoring, improved loop,
button handling
- `FilesPage.html` - WebSocket upload JavaScript with HTTP fallback

## Additional Context

- WebSocket uses 4KB chunks with backpressure management to prevent
ESP32 buffer overflow
- Falls back to HTTP automatically if WebSocket connection fails
- The main bottleneck now is SD card write speed (~44% of transfer
time), not WiFi
- STA mode was more prone to crashes than AP mode due to external
network factors; WiFi health monitoring helps detect and handle
disconnections gracefully

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_ Claude did it
ALL, I have no idea what I am doing, but my books transfer fast now.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 23:11:28 +11:00
Justin Mitchell
847786e342 Fixes issue with Calibre web expecting SSL (#347)
http urls now work with Calibre web

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-14 11:54:14 +00:00
Luke Stein
c2fb8ce55d Update User Guide to reflect release 0.13.1 (#337)
Please note I have not tested the Calibre features and am not yet in a
position to offer detailed documentation of how they work.
2026-01-14 22:48:43 +11:00
Armando Cerna
ed05554d74 feat: Add setting to toggle long-press chapter skip (#341)
## Summary

Adds a new "Long-press Chapter Skip" toggle in Settings to control
whether holding the side buttons skips chapters.

I kept accidentally triggering chapter skips while reading, which caused
me to lose my place in the middle of long chapters.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY **_
2026-01-14 22:47:24 +11:00
Jonas Diemer
9a9dc044ce ifdef around optional fonts to reduce flash size/time. (#339)
## Summary

Adds define to omit optional fonts from the build. This reduces time to
flash from >31s to <13s. Useful for development that doesn't require
fonts. Addresses #193

Invoke it like this during development:
`PLATFORMIO_BUILD_FLAGS="-D OMIT_FONTS" pio run --target upload && pio
device monitor`

Changing the define causes `pio` to do a full rebuild (but it will be
quick if you keep the define).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO
2026-01-14 22:40:40 +11:00
Jonas Diemer
1c027ce2cd Skip BOM character (sometimes used in front of em-dashes) (#340)
## Summary

Skip BOM character (sometimes used in front of em-dashes) - they are not
part of the glyph set and would render `?` otherwise.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_
2026-01-14 22:38:30 +11:00
Eunchurn Park
49f97b69ca Add TXT file reader support (#240)
## Summary

* **What is the goal of this PR?** 

Add support for reading plain text (.txt) files, enabling users to
browse, read, and track progress in TXT documents alongside existing
EPUB and XTC formats.

* **What changes are included?**

- New Txt library for loading and parsing plain text files
- New TxtReaderActivity with streaming page rendering using 8KB chunks
to handle large files without memory issues on ESP32-C3
- Page index caching system (index.bin) for instant re-open after sleep
or app restart
- Progress bar UI during initial file indexing (matching EPUB style)
- Word wrapping with proper UTF-8 support
- Cover image support for TXT files:
- Primary: image with same filename as TXT (e.g., book.jpg for book.txt)
  - Fallback: cover.bmp/jpg/jpeg in the same folder
  - JPG to BMP conversion using existing converter
  - Sleep screen cover mode now works with TXT files
- File browser now shows .txt files

## Additional Context

* Add any other information that might be helpful for the reviewer

* Memory constraints: The streaming approach was necessary because
ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely
into memory, so we read 8KB chunks and build a page offset index
instead.

* Cache invalidation: The page index cache automatically invalidates
when file size, viewport width, or lines per page changes (e.g., font
size or orientation change).

* Performance: First open requires indexing (with progress bar),
subsequent opens load from cache instantly.

* Cover image format: PNG is detected but not supported for conversion
(no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 21:36:40 +11:00
Dave Allie
14643d0225 Move string helpers out of HomeActivity into StringUtils 2026-01-14 21:24:45 +11:00
Eunchurn Park
fecd1849b9 Add cover image display in *Continue Reading* card with framebuffer caching (#200)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module,

Display the book cover image in the **"Continue Reading"** card on the
home screen, with fast navigation using framebuffer caching.

* **What changes are included?**

- Display book cover image in the "Continue Reading" card on home screen
- Load cover from cached BMP (same as sleep screen cover)
- Add framebuffer store/restore functions (`copyStoredBwBuffer`,
`freeStoredBwBuffer`) for fast navigation after initial render
- Fix `drawBitmap` scaling bug: apply scale to offset only, not to base
coordinates
- Add white text boxes behind title/author/continue reading label for
readability on cover
- Support both EPUB and XTC file cover images
- Increase HomeActivity task stack size from 2048 to 4096 for cover
image rendering

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

- Performance: First render loads cover from SD card (~800ms),
subsequent navigation uses cached framebuffer (~instant)
- Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home
screen, freed on exit
- Fallback: If cover image is not available, falls back to standard
text-only display
- The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale
was incorrectly scaling the base coordinates. Now correctly uses screenY
= y + (offset scale)
2026-01-14 21:24:02 +11:00
Will Morrow
2040e088e7 Ensure new custom sleep image every time (#300)
When picking a random sleep image from a set of custom images, compare
the randomly chosen index against a cached value in settings. If the
value matches, use the next image (rolling over if it's the last image).
Cache the chosen image index to settings in either case.

## Summary

Implements a tweak on the custom sleep image feature that ensures that
the user gets a new image every time the device goes to sleep.
This change adds a new setting (perhaps there's a better place to cache
this?) that stores the most recently used file index. During picking the
random image index, we compare this against the random index and choose
the next one (modulo the number of image files) if it matches, ensuring
we get a new image.

## Additional Context

As mentioned, I used settings to cache this value since it is a
persisted store, perhaps that's overkill. Open to suggestions on if
there's a better way.
2026-01-14 21:05:08 +11:00
Andrew Brandt
65d23910a3 ci: add PR format check workflow (#328)
**Description**:

Add a new workflow to check the PR formatting to ensure consistency on
PR titles. We can also use this for semantic release versioning later,
if we so desire.

**Related Issue(s)**:

Implements first portion of #327

---------

Signed-off-by: Andrew Brandt <brandt.andrew89@gmail.com>
2026-01-14 21:04:02 +11:00
Dave Allie
52995fa722 chore: Cut release 0.13.1 2026-01-13 02:09:39 +11:00
Dave Allie
d4f8eda154 fix: Increase home activity stack size (#333)
## Summary

* fix: Increase home activity stack size

## Additional Context

* Home activity can crash occasionally depending on book

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-01-13 02:09:06 +11:00
Dave Allie
33b8fa0e19 chore: Cut release 0.13.0 2026-01-13 00:59:13 +11:00
Dave Allie
16c760b2d2 copy: Tweak pull request template wording 2026-01-13 00:59:04 +11:00
Dave Allie
8f3df7e10e fix: Handle EPUB 3 TOC to spine mapping when nav file in subdirectory (#332)
## Summary

- Nav file in EPUB 3 file is a HTML file with relative hrefs
- If this file exists anywhere but in the same location as the
content.opf file, navigating in the book will fail
- Bump the book cache version to rebuild potentially broken books

## Additional Context

- Fixes https://github.com/daveallie/crosspoint-reader/issues/264

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code?

- [ ] Yes
- [ ] Partially
- [x] No
2026-01-13 00:57:34 +11:00
Jonas Diemer
0165fab581 Fix BMP rendering gamma/brightness (#302)
1. Refactor Bitmap.cpp/h to expose the options for FloydSteinberg and
brightness/gamma correction at runtime
2. Fine-tune the thresholds for Floyd Steiberg and simple quantization
to better match the display's colors

Turns out that 2 is enough to make the images render properly, so the
brightness boost and gamma adjustment doesn't seem necessary currently
(at least for my test image).
2026-01-12 22:36:19 +11:00
danoob
66b100c6ca fix: Wi-Fi Selection on Calibre Library launch (#313)
## Summary

* **What is the goal of this PR?** 
Fixes the Wi-Fi connection issue when launching the Calibre Library
(OPDS browser). The previous implementation always attempted to connect
using the first saved WiFi credential, which caused connection failures
when users were in locations where only other saved networks (not the
first one) were available. Now, the activity launches a WiFi selection
screen allowing users to choose from available networks.

* **What changes are included?**

## Additional Context
**Bug Fixed**: Previously, the code used `credentials[0]` (always the
first saved WiFi), so users in areas with only their secondary/tertiary
saved networks available could never connect.

---------

Co-authored-by: danoooob <danoooob@example.com>
2026-01-12 21:49:42 +11:00
Andrew Brandt
41bda43899 chore: update formatting in github workflows (#320)
**Description**:

The purpose of this change is to modify the spacing in the
`.github/workflow` files to ensure consistency.

**Related Issue(s)**:

Implements #319

Signed-off-by: Andrew Brandt <brandt.andrew89@gmail.com>
2026-01-12 21:37:23 +11:00
Dave Allie
82f21f3c1d Add AI usage question to the PR template 2026-01-12 21:35:18 +11:00
Jonas Diemer
a9242fe61f Generate different .bmp for cropped covers so settings have effect. (#330)
Addresses
https://github.com/daveallie/crosspoint-reader/pull/225#issuecomment-3735150337
2026-01-12 20:55:47 +11:00
Jonas Diemer
88d0d90471 Add option to hide battery percentage. (#297)
with option to always hide or hide in reader only.

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-12 20:53:58 +11:00
David Fischer
97c4871316 Add page turn on power button press (#286)
## Summary

* **What is the goal of this PR?** 
* This PR adds a setting to (additionally) map the forward page turn
onto the powerbutton when in `EPUBReaderActivity` and powerbutton short
press is not mapped to sleep mode. I find the powerbutton to be exactly
where my thumb is while reading so it is very convenient to map the
forwardpage turn to that. Maybe Im not alone with this ^^
* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
2026-01-12 20:07:26 +11:00
Seth
66811bf50b Add navigation hints to ChapterSelectionActivities (#294)
## Summary

Add navigation hints to Chapter Select - #190 

### Before

![Mi 11X_20260108_214114_lmc_8
4](https://github.com/user-attachments/assets/45031d21-2c6c-4b7d-a5cc-6ad111bf5a70)

### After
![Mi 11X_20260108_213803_lmc_8
4](https://github.com/user-attachments/assets/1fa4ef22-63e4-4adb-8fc5-5fb8c7fa79fa)
2026-01-12 19:59:02 +11:00
Samuel Carpentier
87287012ba Updated user guide (sleep screens list) (#293)
## Summary

Updated sleep screens list by adding the newly available "Blank" option
2026-01-12 19:58:08 +11:00
Luke Stein
d4ae108d9b Docs: Add instructions for file management via curl (#282)
Per a [reddit
thread](https://www.reddit.com/r/xteinkereader/comments/1q0fk9r/if_using_crosspoint_firmware_you_can_upload_using/),
the file manager can be accessed via curl.

Given file upload or deletion via curl may be useful for advanced users,
I've added instructions.
2026-01-09 08:58:58 +11:00
Jonas Diemer
7240cd52a9 Move battery status on home screen to top left (#253)
So it doesn't look so lost on a row on its own.

Also sligthly (1px) moved symbol in on reader view.
2026-01-09 08:57:50 +11:00
Dave Allie
0bae3bbf64 Support up to 500 character file names (#275)
## Summary

- Support up to 500 character file names

## Additional Context

- Fixes #265
2026-01-07 23:43:19 +11:00
Dave Allie
2b12a65011 Remove HTML entity parsing (#274)
## Summary

* Remove HTML entity parsing
  * This has been completely useless since the introduction of expat
* expat tries to parse all entities in the document, but only knows of
HTML ones
* Parsing will never end with HTML entities in the text, so the
additional step to parse them that we had went completely unused
* We should figure out the best way to parse that content in the future,
but for now remove that module as it generates a lot of heap allocations
with its map and strings
2026-01-07 23:08:43 +11:00
Dave Allie
46fa186b82 Make extension checks case-insensitive (#273)
## Summary

* Implement new `StringUtils::checkFileExtension` which does case
insensitive checking
* Move all checks over to this
2026-01-07 21:07:23 +11:00
Luke Stein
0cc2c64df2 Update User Guide to reflect release 0.12.0 (#269)
## Summary

* Update the User Guide per current firmware release.
* Make some formatting improvements including for readability and
consistency.
2026-01-07 20:28:32 +11:00
Stanislav Khromov
1f956e972b Allow disabling anti-aliasing via a setting (#241)
## Summary

Fixes https://github.com/daveallie/crosspoint-reader/issues/233

## Additional Context

By disabling the Text Anti-Aliasing in the settings, you can get faster
black-and-white page turns that work exactly like the stock firmware,
instead of having an additional flash after rendering (see issue linked
above for example).

Tested on the X4 and confirmed working.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-07 20:14:35 +11:00
Dave Allie
9c573e6f7f Ensure new settings are at the end of the settings file 2026-01-07 20:02:33 +11:00
Justin
0edb2baced feat: remember parent folder index in menu when ascending folders (#260)
## Summary

Adds feature to file selection activity for better user navigation when
ascending folders. The activity now remembers the index of the parent
folder instead of always resetting to the first element.

I don't have any means of testing this, so if someone could test it
that'd be great

Resolves #259
2026-01-07 19:58:49 +11:00
Justin Mitchell
b792b792bf Calibre Web Epub Downloading + Calibre Wireless Device Syncing (#219)
## Summary

Adds support for browsing and downloading books from a Calibre-web
server via OPDS.
How it works
1. Configure server URL in Settings → Calibre Web URL (e.g.,
https://myserver.com:port I use Cloudflare tunnel to make my server
accessible anywhere fwiw)
2. "Calibre Library" will now show on the the home screen
3. Browse the catalog - navigate through categories like "By Newest",
"By Author", "By Series", etc.
4. Download books - select a book and press Confirm to download the EPUB
to your device
Navigation
- Up/Down - Move through entries
- Confirm - Open folder or download book
- Back - Go to parent catalog, or exit to home if at root
- Navigation entries show with > prefix, books show title and author
- Button hints update dynamically ("Open" for folders, "Download" for
books)
Technical details
- Fetches OPDS catalog from {server_url}/opds
- Parses both navigation feeds (catalog links) and acquisition feeds
(downloadable books)
- Maintains navigation history stack for back navigation
- Handles absolute paths in OPDS links correctly (e.g.,
/books/opds/navcatalog/...)
- Downloads EPUBs directly to the SD card root
Note
The server URL should be typed to include https:// if the server
requires it - HTTP→HTTPS redirects may cause SSL errors on ESP32.

## Additional Context

* I also changed the home titles to use uppercase for each word and
added a setting to change the size of the side margins

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-07 19:58:37 +11:00
Jonas Diemer
afe9672156 Feature/cover crop mode (#225)
Added a setting to select `fit` or `crop` for cover image on sleep
screen.

Might add a `expand` feature in the future that does not crop but rather
fills the blank space with a mirror of the image.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-05 21:07:27 +11:00
David Fischer
9f95b31de5 add settings for reader screen margin (#223)
## Summary

* **What is the goal of this PR?** 
* This PR adds a setting to control the top left and right margins of
the reader screen in 4 sizes (5, 10, 20, 40 pt?) and defaults to `SMALL`
which is equivalent to the fixed margin of 5 that was already in use
before.
* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-05 20:29:08 +11:00
Stanislav Khromov
c76507c937 Add blank sleep screen option (#242)
## Summary

Very small change to add a blank ("None") sleep screen option, for those
who prefer a clean aesthetic.

Tested on X4 device.
2026-01-05 20:08:39 +11:00
Justin
881aa2e005 Fix scrolling wrap-around in settings menu (#249)
## Summary

Fixes a bug in the settings menu, where previously wrap-around only
worked when scrolling upwards. Now, scrolling downwards on the last list
element wraps around to the top as expected.

Resolves #236.
2026-01-05 19:25:27 +11:00
Dave Allie
14972b34cb Remove authentication type from hotspot QR code (#235)
## Summary

* Remove the `T` parameter (authentication type), and `P` parameter
(password) for the SoftAP wifi config QR code

## Additional Context

* It is optional according to the spec:
https://www.wi-fi.org/system/files/WPA3%20Specification%20v3.2.pdf#page=25
so should either be omitted or set to nopass
* Fixes https://github.com/daveallie/crosspoint-reader/issues/229
2026-01-04 16:08:32 +11:00
Jonas Diemer
c8f4870d7c Improved battery symbol (#228)
Rounded corners, smaller positive terminal thingy.
2026-01-04 15:27:34 +11:00
Dave Allie
5fdf23f1d2 Cut release 0.12.0 2026-01-03 19:42:14 +11:00
Justinian
2fb417ee90 Feat/sleep and refresh settings (#209)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
  file uploading.)
* **What changes are included?**

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

---------

Co-authored-by: ratedcounsel <hello@ratedcounsel.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-03 19:33:42 +11:00
V
c8f6160fbc Refactor semantic version comparison for OTA updates (#216)
## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
  file uploading.)

This PR refactors the semantic version comparison logic used during OTA
update checks.

Memory stats before : 
RAM:   [===       ]  30.8% (used 101068 bytes from 327680 bytes)
Flash: [========= ]  85.7% (used **5617830** bytes from 6553600 bytes)

Memory stats before : 
RAM:   [===       ]  30.8% (used 101068 bytes from 327680 bytes)
Flash: [========= ]  85.7% (used **5616870** bytes from 6553600 bytes)


* **What changes are included?**

Replaced std::string::substr() and std::stoi() based parsing with a
lightweight, heap-free approach.
Version parsing is now done in a single pass without creating temporary
std::string objects.
Behavior remains identical: versions are still compared as
MAJOR.MINOR.PATCH.

## Additional Context

`std::string::substr() ` creates a new string and performs heap
allocation

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
2026-01-03 19:19:53 +11:00
Brendan O'Leary
8e4484cd22 Add buton hints to keyboard screen (#205)
## Summary

This adds the correctly styled button hints to the keyboard screen as
well as the ability to add hints to the side buttons (and up/down hints
to that screen)

## Additional Context

N/A
2026-01-03 19:17:53 +11:00
Pavel Liashkov
0332e1103a Add EPUB 3 nav.xhtml TOC support (#197)
## Summary

* **What is the goal of this PR?** Add EPUB 3 support by implementing
native navigation document (nav.xhtml) parsing with NCX fallback,
addressing issue Fixes: #143.

  * **What changes are included?**
- New `TocNavParser` for parsing EPUB 3 HTML5 navigation documents
(`<nav epub:type="toc">`)
- Detection of nav documents via `properties="nav"` attribute in OPF
manifest
- Fallback logic: try EPUB 3 nav first, fall back to NCX (EPUB 2) if
unavailable
- Graceful degradation: books without any TOC now load with a warning
instead of failing

  ## Additional Context

* The implementation follows the existing streaming XML parser pattern
using Expat to minimize RAM usage on the ESP32-C3
* EPUB 3 books that include both nav.xhtml and toc.ncx will prefer the
nav document (per EPUB 3 spec recommendation)
* No breaking changes - existing EPUB 2 books continue to work as before
* Tested on examples from
https://idpf.github.io/epub3-samples/30/samples.html
2026-01-03 19:10:35 +11:00
Jonas Diemer
5790d6f5dc Subtract time it took reaching the evaluation from the press duration. (#208)
Adresses #206
2026-01-03 18:54:23 +11:00
Jake Lyell
062d69dc2a Add support for uploading multiple epubs (#202)
Upload multiple files at once in sequence. Add retry button for files
that fail

## Summary

* **What is the goal of this PR?**
Add support for selecting multiple epub files in one go, before
uploading them all to the device
* **What changes are included?**
Allow multiple selections to be submitted to the input field.
Sends each file to the device one by one in a loop
Adds retry logic and UI for easy re-trying of failed uploads

Addresses #201 


button now says "Choose Files", and shows the number of files you
selected
<img width="506" height="199" alt="image"
src="https://github.com/user-attachments/assets/64b0b921-1e67-438e-9cd7-57d5466f2456"
/>

Shows which file is uploading:
<img width="521" height="283" alt="image"
src="https://github.com/user-attachments/assets/17b4d349-0698-4712-984c-b72fcdcb0918"
/>

Failed upload dialog:
<img width="851" height="441" alt="image"
src="https://github.com/user-attachments/assets/e8bf4aa6-d3d2-4c0b-9c7a-420e8c413033"
/>
<img width="834" height="641" alt="image"
src="https://github.com/user-attachments/assets/656a9732-3963-4844-94e3-4d8736f6d9d5"
/>
2026-01-02 18:32:26 +11:00
Maeve Andrews
5e9626eb2a Add paragraph alignment setting (justify/left/center/right) (#191)
## Summary

* **What is the goal of this PR?** 

Add a new user setting for paragraph alignment, instead of hard-coding
full justification.

* **What changes are included?**

One new line in the settings screen, with 4 options
(justify/left/center/right). Default is justified since that's what it
was already. I personally only wanted to disable justification and use
"left", but I included the other options for completeness since they
were already supported.

## Additional Context

Tested on my X4 and looks as expected for each alignment.

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-02 18:21:48 +11:00
Jonas Diemer
00e83af4e8 Show "Entering Sleep" on black, so it's quicker to notice (in book). (#181)
Black popup is easier to notice at higher contrast.
2026-01-02 17:55:21 +11:00
Jonas Diemer
39080c0e51 Skip soft hyphens. (#195)
For now, let's skip the soft hyphens (later, we can treat them in the
layouter). See
https://github.com/daveallie/crosspoint-reader/discussions/17#discussioncomment-15378475
2026-01-02 17:54:46 +11:00
Brendan O'Leary
9e59a5106b Fix race condition with keyboard and Wifi entry (#204)
## Summary

* **What is the goal of this PR?** Fixes a bug -
https://github.com/daveallie/crosspoint-reader/issues/187 - where the
screen would freeze after entering a WiFi password, causing the device
to appear hung.

* **What changes are included?**
- Fixed a race condition in `WifiSelectionActivity::displayTaskLoop()`
that caused rendering of an empty screen when transitioning from the
keyboard subactivity
- Added `vTaskDelay()` when a subactivity is active to prevent CPU
starvation from a tight `continue` loop
- Added a check to skip rendering when in `PASSWORD_ENTRY` state,
allowing the state machine to properly transition to `CONNECTING` before
the display updates

## Additional Context

* **Root cause:** When the keyboard subactivity exited after password
entry, the display task would wake up and attempt to render. However,
the `state` was still `PASSWORD_ENTRY` (before `attemptConnection()`
changed it to `CONNECTING`), and since there was no render case for
`PASSWORD_ENTRY`, the display would show a cleared/empty buffer,
appearing frozen.

* **Performance implications:** The added `vTaskDelay(10)` calls when
subactivity is active or in `PASSWORD_ENTRY` state actually improve
performance by preventing CPU starvation - previously the display task
would spin in a tight loop with `continue` while a subactivity was
present.

* **Testing focus:** Test the full WiFi connection flow:
  1. Enter network selection
  2. Select a network requiring a password
  3. Enter password and press OK
  4. Verify "Connecting..." screen appears
  5. Verify connection completes and prompts to save password
2026-01-02 17:49:16 +11:00
Pavel Liashkov
a922e553ed Prevent device sleep during WiFi file transfer and OTA updates (#203)
## Summary

* **What is the goal of this PR?** Fixes #199 - Device falls asleep
during WiFi file transfer after 10 minutes of inactivity, disconnecting
the web server.

  * **What changes are included?**
    - Add `preventAutoSleep()` virtual method to `Activity` base class
- Modify main loop to reset inactivity timer when `preventAutoSleep()`
returns true
- Override `preventAutoSleep()` in `CrossPointWebServerActivity`
(returns true when web server running)
- Override `preventAutoSleep()` in `OtaUpdateActivity` (returns true
during update check/download)

  ## Additional Context

* The existing `skipLoopDelay()` method controls loop timing (yield vs
delay) for HTTP responsiveness. The new `preventAutoSleep()` method is
semantically separate - it explicitly signals that an activity should
keep the device awake.
* `CrossPointWebServerActivity` uses both methods: `skipLoopDelay()` for
responsive HTTP handling, `preventAutoSleep()` for staying awake.
* `OtaUpdateActivity` only needs `preventAutoSleep()` since the OTA
library handles HTTP internally.
2026-01-02 17:44:17 +11:00
Dave Allie
04ad4e5aa4 Replace book and section bin format images with ImHex hexpat definition (#189)
## Summary

* Replace book and section bin format images with ImHex hexpat
definition
* This should give readers an understanding of the file format but also
supply some utility when validating content/output
2025-12-31 13:28:24 +11:00
Dave Allie
6e9ba1006a Use sane smaller data types for data in section.bin (#188)
## Summary

* Update EpdFontFamily::Style to be u8 instead of u32 (saving 3 bytes
per word)
* Update layout width/height to be u16 from int
* Update page element count to be u16 from u32
* Update text block element count to be u16 from u32
* Bumped section bin version to version 8
2025-12-31 13:11:36 +11:00
Luke Stein
40f9ed485c Enhance USER_GUIDE with links and clarifications (#185)
Updated USER_GUIDE.md for clarity and added links to sections.

## Summary

* Clarify and improve documentation
2025-12-31 10:01:48 +11:00
Dave Allie
b82e044ac3 Cut release 0.11.2 2025-12-31 09:28:14 +11:00
Dave Allie
026733a4fe Hide "System Volume Information" folder (#184)
## Summary

* Hide "System Volume Information" folder
* It's a FAT filesystem folder, shouldn't be used

## Additional Context

* Fixes https://github.com/daveallie/crosspoint-reader/issues/177
2025-12-31 09:27:17 +11:00
Dave Allie
57b075ec97 Update README.md features 2025-12-31 09:18:05 +11:00
dangson
648c688642 Update button hints on OTA update screen and update user guide to reflect current settings (#183)
## Summary

- Update the button hints on the OTA update screen to reflect the
current front button layout.
- Update user guide to reflect current available settings. A lot has
been added recently!
2025-12-31 09:15:40 +11:00
Jonas Diemer
06065dfd8b Show book title instead of "Select Chapter". (#169)
I think this is nicer ;)
2025-12-31 09:10:41 +11:00
Eunchurn Park
93226c9fbb Fix file browser navigation for non-ASCII folder names (#178)
## Summary

* **What is the goal of this PR?**

Fix file browser failing to navigate into subdirectories with non-ASCII
(Korean/Unicode) folder names.

* **What changes are included?**

- Enable UTF-8 long file names in SdFat (`USE_UTF8_LONG_NAMES=1`)
- Add directory validation before iterating files
- Add `rewindDirectory()` call for stability

## Additional Context
2025-12-31 09:08:31 +11:00
Dave Allie
941643cf97 Cut release 0.11.1 2025-12-31 02:47:24 +11:00
Dave Allie
9bba41ed96 Move home screen battery indicator to avoid clashing with button hints (#174)
## Summary

* Move home screen battery indicator to avoid clashing with button hints
* Default button mapping was fine, but others clashes with the indicator
2025-12-31 02:46:46 +11:00
Dave Allie
34cf5f0636 Add button mapping for Left, Back, Confirm, Right (#173)
## Summary

* Add button mapping for Left, Back, Confirm, Right for front buttons

## Additional Context

* Asked for in
https://github.com/daveallie/crosspoint-reader/discussions/78#discussioncomment-15375326
2025-12-31 02:46:35 +11:00
Dave Allie
f2ca65d752 Swap from Aleo to Bookerly for wider glyph support (#172)
## Summary

* Swap from Aleo to Bookerly for wider glyph support
* Swap from Space Grotesk to a small Noto Sans

## Additional Context

* 0.11.0 swapped to Aleo which has a few issues (things like Cyrillic
support for eg)
2025-12-31 02:28:25 +11:00
Dave Allie
6a8971fc20 Cut release 0.11.0 2025-12-30 23:42:19 +11:00
Dave Allie
e2cba5be83 Show battery percentage on home screen (#167)
## Summary

* Show battery percentage on home screen
  * Moved battery rendering logic into shared ScreenComponents class
* As discussed
https://github.com/daveallie/crosspoint-reader/discussions/155
2025-12-30 23:41:47 +11:00
Dave Allie
52a0b5bbe9 Small cleanups from https://github.com/juicecultus/crosspoint-reader-x4 2025-12-30 23:19:08 +11:00
Dave Allie
3abcd0d05d Redesign home screen (#166)
## Summary

* Redesigned home screen with big option to continue reading and
slightly nicer options to navigate to core sections
* Attempt to use the cached EPUB details (title, author) if they exist,
otherwise fall back to file name
* Adjusted button hints on home screen, removed Back option and changed
left/right to up/down

## Additional Context

* Core of this work comes from @ChandhokTannay in
1d36a86ef1
2025-12-30 23:18:10 +11:00
Jonas Diemer
03f0ce04cc Feature: go to text/start reference in epub guide section at first start (#156)
This parses the guide section in the content.opf for text/start
references and jumps to this on first open of the book.

Currently, this behavior will be repeated in case the reader manually
jumps to Chapter 0 and then re-opens the book. IMO, this is an
acceptable edge case (for which I couldn't see a good fix other than to
drag a "first open" boolean around).

---------

Co-authored-by: Sam Davis <sam@sjd.co>
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-30 23:02:46 +11:00
Dave Allie
be1b5bad21 Parse the author name from content.opf file (#165)
## Summary

* Parse the author name from content.opf file
  * Listed in the dc:creator tag within the metadata section
2025-12-30 22:15:44 +11:00
Dave Allie
3dd52f30fa Adjust screen title position 2025-12-30 22:06:57 +11:00
Dave Allie
e43fec79be Add setting for line spacing to adjust space between lines (#164)
## Summary

* Add setting for line spacing to adjust space between lines
* Aleo is already a bit tighter than Noto Sans and Open Dyslexic, so
have adjusted the values to match, this can be tweaked in the future
2025-12-30 19:34:46 +11:00
Dave Allie
bf7bffd506 Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary

* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
  * They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
  * Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo

## Additional Context

Line compression setting will follow

| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |
![IMG_5704](https://github.com/user-attachments/assets/7acb054f-ddef-4080-b3c8-590cfaf13115)
|
![IMG_5705](https://github.com/user-attachments/assets/d4819036-5c89-486e-92c3-86094fa4d89a)
|
![IMG_5706](https://github.com/user-attachments/assets/35caf622-d126-4396-9c3e-f927eba1e1f4)
|
![IMG_5707](https://github.com/user-attachments/assets/af32370a-6244-400f-bea9-5c27db040b5b)
|
| Noto Sans |
![IMG_5708](https://github.com/user-attachments/assets/1f9264a5-c069-4e22-9099-a082bfcaabc5)
|
![IMG_5709](https://github.com/user-attachments/assets/ef6b07fe-8d87-403a-b152-05f50b69b78e)
|
![IMG_5710](https://github.com/user-attachments/assets/112a5d20-262c-4dc0-b67d-980b237e4607)
|
![IMG_5711](https://github.com/user-attachments/assets/d25e0e1d-2ace-450d-96dd-618e4efd4805)
|
| Open Dyslexic |
![IMG_5712](https://github.com/user-attachments/assets/ead64690-f261-4fae-a4a2-0becd1162e2d)
|
![IMG_5713](https://github.com/user-attachments/assets/59d60f7d-5142-4591-96b0-c04e0a4c6436)
|
![IMG_5714](https://github.com/user-attachments/assets/bb6652cd-1790-46a3-93ea-2b8f70d0d36d)
|
![IMG_5715](https://github.com/user-attachments/assets/496e7eb4-c81a-4232-83e9-9ba9148fdea4)
|
2025-12-30 19:21:47 +11:00
Dave Allie
9f31f80c80 Show previous title for unnamed spines (#158)
## Summary

* Show previous title for unnamed spines
* The spec is a little unclear, but there are plenty of cases where
chapters are split up in parts and should show the previous chapter's
title
* List TOC items instead of spine items in chapter select
* Bump `BOOK_CACHE_VERSION` to `2` to force regeneration of spine item's
TOC indexes
2025-12-30 18:52:42 +11:00
Dave Allie
fb5fc32c5d Add exFAT support (#150)
## Summary

* Swap to updated SDCardManager which uses SdFat
* Add exFAT support
  * Swap to using FsFile everywhere
* Use newly exposed `SdMan` macro to get to static instance of
SDCardManager
* Move a bunch of FsHelpers up to SDCardManager
2025-12-30 16:09:30 +11:00
Jonas Diemer
d4bd119950 Add option to apply format fix only on changed files (much faster) (#153)
The default version parses a lot of files and takes ~5s on my machine.
This adds an option `-g` to run only on files modified/staged in git.
2025-12-30 16:05:06 +11:00
Dave Allie
85d76da967 Split XTC file version into major minor bytes (#161)
## Summary

* Split XTC file version into major minor bytes
  * Continue to support both 1.0 and 0.1

## Additional Context

* See
https://github.com/daveallie/crosspoint-reader/issues/146#issuecomment-3698223951
for more detail

FYI @treetrum @eunchurn
2025-12-30 15:48:20 +11:00
Dave Allie
e4ac90f5c1 Accept big endian version in XTC files (#159)
## Summary

* Accept big endian version in XTC files
* Recently, two issues
(https://github.com/daveallie/crosspoint-reader/issues/157 and
https://github.com/daveallie/crosspoint-reader/issues/146) have popped
up with XTC files with a big endian encoded version. This is read out as
256.
  * We should be more lax and accept these values.
2025-12-30 13:36:25 +11:00
Sam Davis
278b056bd0 Add chapter select support to XTC files (#145)
## Summary

- **What is the goal of this PR?** Add chapter selection support to the
XTC reader activity, including parsing chapter metadata from XTC files.
- **What changes are included?** Implemented XTC chapter parsing and
exposure in the XTC library, added a chapter selection activity for XTC,
integrated it into XtcReaderActivity, and normalized chapter page
indices by shifting them to 0-based.

  ## Additional Context

- The reader uses 0-based page indexing (first page = 0), but the XTC
chapter table appears to be 1-based (first page = 1), so chapter
start/end pages are shifted down by 1 during parsing.
2025-12-30 12:49:18 +11:00
Jonas Diemer
b01eb50325 Shorten continueLabel to actual screen width. (#151) 2025-12-29 23:18:23 +11:00
Jonas Diemer
1bfe694807 Improvement/settings selection by inversion (#152)
Style in settings like everywhere else. Also improved the alignment of
the settings value.
2025-12-29 23:18:16 +11:00
Jonas Diemer
7b32a87596 Recalibrated power button duration, decreased long setting slightly. (#149)
Slight tuning, as I noticed sometimes inconsistent behavior (reported
>200ms of calibration value, I assume related to the Serial output).
2025-12-29 23:18:12 +11:00
Dave Allie
071ccb9d1b Custom zip parsing (#140)
## Summary

* Use custom zip central directory parsing to lower memory usage when
loading zipped epub content
2025-12-29 21:17:29 +11:00
Yona
d7f4bd54f5 Add side button layout configuration while on reader (#147)
## Summary

Allow swapping the side button layout between *next page - prev page*
and *prev page - next page* while reading
2025-12-29 21:17:10 +11:00
Dave Allie
2437943c94 Remove usused module 2025-12-29 21:07:26 +11:00
dangson
140d8749a6 Support swapping the functionality of the front buttons (#133)
## Summary

**What is the goal of this PR?** 

Adds a setting to swap the front buttons. The default functionality are:
Back/Confirm/Left/Right. When this setting is enabled they become:
Left/Right/Back/Confirm. This makes it more comfortable to use when
holding in your right hand since your thumb can more easily rest on the
next button. The original firmware has a similar setting.

**What changes are included?**

- Add the new setting.
- Create a mapper to dynamically switch the buttons based on the
setting.
- Use mapper on the various activity screens.
- Update the button hints to reflect the swapped buttons.

## Additional Context

Full disclosure: I used Codex CLI to put this PR together, but did
review it to make sure it makes sense.

Also tested on my device:
https://share.cleanshot.com/k76891NY
2025-12-29 14:59:14 +11:00
Dave Allie
534504cf7a Consolidate chapter page data into single file (#144)
## Summary

* Consolidate chapter page data into single file
* Header structure of the file stays the same, following the page count,
we now put a LUT offset
   * The page data is all then appended to this file
* Finally the LUT is appended to the end of the file, and the page count
is updated
* This will also significantly improve the duration of cache cleanup
which takes a while to scan the directory and cleanup content
* Remove page file version as it's all tied up into the section file now
* Bumped section file version to 7
* Moved section content into sub directory
* Updated docs

## Additional Context

* Benchmarks:
  * Generating 74 pages of content from a chapter in Jade Legacy took:
    * master: 6,229ms
    * this PR: 1,305ms
    * Speedup of 79%
  * Generating 207 pages of content from Livesuit book:
    * With progress bar UI updates:
      * master: 24,250ms
      * this PR: 8,063ms
      * Speedup of 67%
    * Without progress bar UI updates:
      * master: 13,055ms
      * this PR: 3,600ms
      * Speedup of 72%
2025-12-29 13:19:54 +11:00
Dave Allie
b1763821b5 Cut release 0.10.0 2025-12-29 02:30:27 +11:00
Dave Allie
c0b83b626e Use a JSON filter to avoid crashes when checking for updates (#141)
## Summary

* The JSON release data from Github contains the entire release
description which can be very large
  * The 0.9.0 release was especially bad
* Use a JSON filter to avoid deserializing anything but the necessary
fields

## Additional Context

*
https://arduinojson.org/v7/how-to/deserialize-a-very-large-document/#filtering
* Fixes https://github.com/daveallie/crosspoint-reader/issues/124
2025-12-29 02:29:41 +11:00
Dave Allie
f8c0b1acea Use confirmation release on home screen to detect action 2025-12-29 02:00:42 +11:00
Eunchurn Park
f9b604f04e Add XTC/XTCH ebook format support (#135)
## Summary

* **What is the goal of this PR?**

Add support for XTC (XTeink X4 native) ebook format, which contains
pre-rendered 480x800 1-bit bitmap pages optimized for e-ink displays.

* **What changes are included?**

- New `lib/Xtc/` library with XtcParser for reading XTC files
- XtcReaderActivity for displaying XTC pages on e-ink display
- XTC file detection in FileSelectionActivity
- Cover BMP generation from first XTC page
- Correct XTG page header structure (22 bytes) and bit polarity handling

## Additional Context

- XTC files contain pre-rendered bitmap pages with embedded status bar
(page numbers, progress %)
- XTG page header: 22 bytes (magic + dimensions + reserved fields +
bitmap size)
- Bit polarity: 0 = black, 1 = white
- No runtime text rendering needed - pages display directly on e-ink
- Faster page display compared to EPUB since no parsing/rendering
required
- Memory efficient: loads one page at a time (48KB per page)
- Tested with XTC files generated from https://x4converter.rho.sh/
- Verified correct page alignment and color rendering
- Please report any issues if you test with XTC files from other
sources.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-29 01:56:05 +11:00
Dave Allie
3dc5f6fec4 Avoid jumping straight into chapter selection screen 2025-12-28 23:49:51 +11:00
Dave Allie
41c93e4eba Use font ascender height for baseline offset (#139)
## Summary

* Use font ascender height for baseline offset
* Previously was using font height, but when rendering the font (even
from y = 0), there would be a lot of top margin
* Font would also go below the "bottom of the line" as we were using the
full font height as the baseline

## Additional Context

* This caused some text to move around, I've fixed everything I can
* Notably it moves the first line of font a little closer to the top of
the page
2025-12-28 22:30:01 +11:00
Dave Allie
1c33162368 Fix rendering issue with entering keyboard from wifi screen 2025-12-28 21:50:45 +11:00
Dave Allie
27d42fbef3 Allow entering into chapter select screen correctly 2025-12-28 21:50:36 +11:00
Tannay
dd280bdc97 Rotation Support (#77)
•  What is the goal of this PR?  
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.

•  What changes are included?
◦  Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦  Settings / Configuration
▪  Extended CrossPointSettings with:
▪  landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪  Updated SettingsActivity to expose two new toggles:
▪  “Landscape Reading”
▪  “Flip Landscape (swap top/bottom)”
◦  EPUB Reader
▪  In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦  EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪  Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.



Additional Context

•  Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
•  Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
•  Testing suggestions / areas to focus on
◦  Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪  Landscape reading in both directions:
▪  Landscape Reading = ON, Flip Landscape = OFF.
▪  Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦  Open the same book:
▪  In portrait first, then switch to landscape and reopen it.
▪  Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 21:33:20 +11:00
Dave Allie
bf031fd999 Fix exiting WifiSelectionActivity renderer early 2025-12-28 19:27:00 +11:00
Dave Allie
02350c6a9f Fix underscore on keyboard and standardize activity (#138)
## Summary

* Fix underscore on keyboard
  * Remove special handling of special row characters
* Fix navigating between special row items
* Standardize keyboard activity to use standard loop
  * Fix issue with rendering keyboard non-stop

Fixes https://github.com/daveallie/crosspoint-reader/issues/131
2025-12-28 18:57:06 +11:00
Dave Allie
9023b262a1 Fix issue where pressing back from chapter select would leave book (#137)
## Summary

* Fix issue where pressing back from chapter select would leave book
* Rely on `wasReleased` checks instead
2025-12-28 17:06:18 +11:00
Eunchurn Park
eabd149371 Add retry logic and progress bar for chapter indexing (#128)
## Summary

* **What is the goal of this PR?**

Improve reliability and user experience during chapter indexing by
adding retry logic for SD card operations and a visual progress bar.

* **What changes are included?**

- **Retry logic**: Add 3 retry attempts with 50ms delay for ZIP to SD
card streaming to handle timing issues after display refresh
- **Progress bar**: Display a visual progress bar (0-100%) during
chapter indexing based on file read progress, updating every 10% to
balance responsiveness with e-ink display limitations

## Additional Context

* **Problem observed**: When navigating quickly through books with many
chapters (before chapter titles finish rendering), the "Indexing..."
screen would appear frozen. Checking the serial log revealed the
operation had silently failed, but the UI showed no indication of this.
Users would likely assume the device had crashed. Pressing the next
button again would resume operation, but this behavior was confusing and
unexpected.

* **Solution**:
- Retry logic handles transient SD card timing failures automatically,
so users don't need to manually retry
- Progress bar provides visual feedback so users know indexing is
actively working (not frozen)

* **Why timing issues occur**: After display refresh operations, there
can be timing conflicts when immediately starting SD card write
operations. This is more likely to happen when rapidly navigating
through chapters.

* **Progress bar design**: Updates every 10% to avoid excessive e-ink
refreshes while still providing meaningful feedback during long indexing
operations (especially for large chapters with CJK characters).

* **Performance**: Minimal overhead - progress calculation is simple
byte counting, and display updates use `FAST_REFRESH` mode.
2025-12-28 15:59:44 +11:00
1991AcuraLegend
838246d147 Add setting to enable status bar display options (#111)
Add setting toggle that allows status bar display options in EpubReader.

Supported options would be as follows: 

- FULL: display as is today
- PROGRESS: display progress bar only
- BATTERY: display battery only
- NONE: hide status bar

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 10:48:27 +11:00
Eunchurn Park
f96b6ab29c Improve EPUB cover image quality with pre-scaling and Atkinson dithering (#116)
## Summary

* **What is the goal of this PR?**

Replace simple threshold-based grayscale quantization with ordered
dithering using a 4x4 Bayer matrix. This eliminates color banding
artifacts and produces smoother gradients on e-ink display.

* **What changes are included?**

- Add 4x4 Bayer dithering matrix for 16-level threshold patterns
- Modify `grayscaleTo2Bit()` function to accept pixel coordinates and
apply position-based dithering
- Replace simple `grayscale >> 6` threshold with ordered dithering
algorithm that produces smoother gradients

## Additional Context

* Bayer matrix approach: The 4x4 Bayer matrix creates a repeating
pattern that distributes quantization error spatially, effectively
simulating 16 levels of gray using only 4 actual color levels (black,
dark gray, light gray, white).

* Cache invalidation: Existing cached `cover.bmp` files will need to be
deleted to see the improved rendering, as the converter only runs when
the cache is missing.
2025-12-28 10:38:14 +11:00
Brendan O'Leary
e3d0201365 Add 'Open' button hint to File Selection page (#136)
## Summary

In using my build of
https://github.com/daveallie/crosspoint-reader/pull/130 I realized that
we need a "open" button hint above the second button in the File browser

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
2025-12-28 10:36:26 +11:00
Eunchurn Park
286b47f489 fix(parser): remove MAX_LINES limit that truncates long chapters (#132)
## Summary

* **What is the goal of this PR?** Fixes a bug where text disappears
after approximately 25 pages in long chapters during EPUB indexing.

* **What changes are included?**
- Removed the `MAX_LINES = 1000` hard limit in
`ParsedText::computeLineBreaks()`
- Added safer infinite loop prevention by checking if `nextBreakIndex <=
currentWordIndex` and forcing advancement by one word when stuck

## Additional Context

* **Root cause:** The `MAX_LINES = 1000` limit was introduced to prevent
infinite loops, but it truncates content in long chapters. For example,
a 93KB chapter that generates ~242 pages (~9,680 lines) gets cut off at
~1000 lines, causing blank pages after page 25-27.

* **Solution approach:** Instead of a hard line limit, I now detect when
the line break algorithm gets stuck (when `nextBreakIndex` doesn't
advance) and force progress by moving one word at a time. This preserves
the infinite loop protection while allowing all content to be rendered.

* **Testing:** Verified with a Korean EPUB containing a 93KB chapter -
all 242 pages now render correctly without text disappearing.
2025-12-28 10:35:45 +11:00
Dave Allie
aff4dc6628 Fix QRCode import attempt 2 2025-12-26 11:33:41 +10:00
Dave Allie
98a39374e8 Fix QRCode import 2025-12-26 11:29:27 +10:00
Jonas Diemer
e8c0fb42d4 Network details QR code (#113)
Using QRCode library from pio to generate the QR code.

Done:
- Display QR code for URL in network mode
- minor fixes of layout
- Display QR for URL in AP mode
- Display QR for AP in AP mode

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-26 12:13:40 +11:00
Eunchurn Park
b77af16caa Add Continue Reading menu and remember last book folder (#129)
## Summary

* **What is the goal of this PR?**

Add a "Continue Reading" feature to improve user experience when
returning to a previously opened book.

* **What changes are included?**

- Add dynamic "Continue: <book name>" menu item in Home screen when a
book was previously opened

- File browser now starts from the folder of the last opened book
instead of always starting from root directory
- Menu dynamically shows 3 or 4 items based on reading history:
  - Without history: `Browse`, `File transfer`, `Settings`
- With history: `Continue: <book>`, `Browse`, `File transfer`,
`Settings`

## Additional Context

* This feature leverages the existing `APP_STATE.openEpubPath` which
already persists the last opened book path
* The Continue Reading menu only appears if the book file still exists
on the SD card
* Book name in the menu is truncated to 25 characters with "..." suffix
if too long
* If the last book's folder was deleted, the file browser gracefully
falls back to root directory
* No new dependencies or significant memory overhead - reuses existing
state management
2025-12-26 11:55:23 +11:00
Brendan O'Leary
e3c1e28b8f Normalize button hints (#130)
## Summary

This creates a `renderer.drawButtonHints` to make all of the "hints"
over buttons to match the home screen.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-26 11:54:02 +11:00
Eunchurn Park
dc7544d944 Optimize glyph lookup with binary search (#125)
Replace linear O(n) search with binary search O(log n) for unicode
interval lookup. Korean fonts have many intervals (~30,000+ glyphs), so
this improves text rendering performance during page navigation.

## Summary

* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module, Implements the new feature for
  file uploading.)

Replace linear `O(n)` glyph lookup with binary search `O(log n)` to
improve text rendering performance during page navigation.

* **What changes are included?**

- Modified `EpdFont::getGlyph()` to use binary search instead of linear
search for unicode interval lookup
- Added early return for empty interval count

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).

- Performance implications: Fonts with many unicode intervals benefit
the most. Korean fonts have ~30,000+ glyphs across multiple intervals,
but any font with significant glyph coverage (CJK, extended Latin,
emoji, etc.) will see improvement.
- Complexity: from `O(n)` to `O(log n)` where n = number of unicode
intervals. For fonts with 10+ intervals, this reduces lookup iterations
significantly.
- Risk: Low - the binary search logic is straightforward and the
intervals are already sorted by unicode codepoint (required for the
original early-exit optimization).
2025-12-26 11:46:17 +11:00
Dave Allie
504c7b307d Cut release 0.9.0 2025-12-24 21:49:47 +10:00
Dave Allie
b6bc1f7ed3 New book.bin spine and table of contents cache (#104)
## Summary

* Use single unified cache file for book spine, table of contents, and
core metadata (title, author, cover image)
* Use new temp item store file in OPF parsing to store items to be
rescaned when parsing spine
  * This avoids us holding these items in memory
* Use new toc.bin.tmp and spine.bin.tmp to build out partial toc / spine
data as part of parsing content.opf and the NCX file
  * These files are re-read multiple times to ultimately build book.bin

## Additional Context

* Spec for file format included below as an image
* This should help with:
  * #10 
  * #60 
  * #99
2025-12-24 22:36:13 +11:00
Dave Allie
ea0abaf351 Prevent SD card error causing boot loop (#122)
## Summary

* Prevent SD card error causing boot loop
* We need the screen and fonts to be initialized to show the full screen
error message
* Prior to this change, trying to render the font would crash the
firmware and boot loop it
2025-12-24 22:33:21 +11:00
Dave Allie
2771579007 Add support for blockquote, strong, and em tags (#121)
## Summary

* Add support for blockquote, strong, and em tags
2025-12-24 22:33:17 +11:00
Dave Allie
27035b2b91 Handle 16x16 MCU blocks in JPEG decoding (#120)
## Summary

* Handle 16x16 MCU blocks in JPEG decoding
* We were only correctly handling 8x8 blocks, which means that we did
not correctly support a lot of JPGs leading to an interlacing style on
the images

## Additional Context

* Fixes https://github.com/daveallie/crosspoint-reader/issues/118
2025-12-24 22:21:41 +11:00
Dave Allie
1107590b56 Standardize File handling with FsHelpers (#110)
## Summary

* Standardize File handling with FsHelpers
* Better central place to manage to logic of if files exist/open for
reading/writing
2025-12-23 14:14:10 +11:00
Dave Allie
66ddb52103 Pin espressif32 platform version 2025-12-23 12:17:12 +11:00
Brendan O'Leary
9f4f71fabe Add AP mode option for file transfers (#98)
## Summary

* **What is the goal of this PR?** Adds WiFi Access Point (AP) mode
support for File Transfer, allowing the device to create its own WiFi
network that users can connect to directly - useful when no existing
WiFi network is available. And in my experience is faster when the
device is right next to your laptop (but maybe further from your wifi)

* **What changes are included?**
- New `NetworkModeSelectionActivity` - an interstitial screen asking
users to choose between:
- "Join a Network" - connects to an existing WiFi network (existing
behavior)
- "Create Hotspot" - creates a WiFi access point named
"CrossPoint-Reader"
  - Modified `CrossPointWebServerActivity` to:
    - Launch the network mode selection screen before proceeding
- Support starting an Access Point with mDNS (`crosspoint.local`) and
DNS server for captive portal behavior
    - Display appropriate connection info for both modes
- Modified `CrossPointWebServer` to support starting when WiFi is in AP
mode (not just STA connected mode)

## Additional Context

* **AP Mode Details**: The device creates an open WiFi network named
"CrossPoint-Reader". Once connected, users can access the file transfer
page at `http://crosspoint.local/` or `http://192.168.4.1/`
* **DNS Captive Portal**: A DNS server redirects all domain requests to
the device's IP, enabling captive portal behavior on some devices
* **mDNS**: Hostname resolution via `crosspoint.local` is enabled for
both AP and STA modes
* **No breaking changes**: The "Join a Network" option preserves the
existing WiFi connection flow
* **Memory impact**: Minimal - the AP mode uses roughly the same
resources as STA mode
2025-12-22 17:24:14 +11:00
Dave Allie
d23020e268 OTA updates (#96)
## Summary

* Adds support for OTA
  * Gets latest firmware bin from latest GitHub release
* I have noticed it be a little flaky unpacking the JSON and
occasionally failing to start
2025-12-22 17:16:46 +11:00
Dave Allie
f4491875ab Thoroughly deinitialise expat parsers before freeing them (#103)
## Summary

* Thoroughly deinitialise expat parsers before freeing them
* Spotted a few crashes when de-initing expat parsers
2025-12-22 17:16:39 +11:00
Dave Allie
6fe28da41b Cut release 0.8.1 2025-12-22 03:20:22 +11:00
Dave Allie
689b539c6b Stream CrossPointWebServer data over JSON APIs (#97)
## Summary

* HTML files are now static, streamed directly to the client without
modification
* For any dynamic values, load via JSON APIs
* For files page, we stream the JSON content as we scan the directory to
avoid holding onto too much data

## Additional details

* We were previously building up a very large string all generated on
the X4 directly, we should be leveraging the browser
* Fixes https://github.com/daveallie/crosspoint-reader/issues/94
2025-12-22 03:19:49 +11:00
Jonas Diemer
ce37c80c2d Improve power button hold measurement for boot (#95)
Improves the duration for which the power button needs to be held - see
#53.

I left the measurement code for the calibration value in, as it will
likely change if we move the settings to NVS.
2025-12-22 00:53:55 +11:00
Dave Allie
b39ce22e54 Cleanup of activities 2025-12-22 00:48:16 +11:00
Dave Allie
77c655fcf5 Give activities names and log when entering and exiting them (#92)
## Summary

* Give activities name and log when entering and exiting them
* Clearer logs when attempting to debug, knowing where users are coming
from/going to helps
2025-12-21 21:17:00 +11:00
Dave Allie
246afae6ef Start power off sequence as soon as hold duration for the power button is reached (#93)
## Summary

* Swap from `wasReleased` to `isPressed` when checking power button
duration
  * In practice it makes the power down experience feel a lot snappier
* Remove the unnecessary 1000ms delay when powering off

## Additional Context

* A little discussion in here:
https://github.com/daveallie/crosspoint-reader/discussions/53#discussioncomment-15309707
2025-12-21 21:16:41 +11:00
Dave Allie
fcfa10bb1f Cut release 0.8.0 2025-12-21 19:02:21 +11:00
Arthur Tazhitdinov
febf79a98a Fix: restores cyrillic glyphs to Pixel Arial font (#70)
## Summary

* adds cyrillic glyphs to pixel arial font, used as Small font in UI

## Additional Context

* with recent changes pixel arial font lost cyrillic glyphs
2025-12-21 19:01:11 +11:00
Dave Allie
424104f8ff Fix incorrect justification of last line in paragraph (#90)
## Summary

* Fix incorrect justification of last line in paragraph
* `words` is changing size due to the slice, so `isLastLine` would
rarely be right, either removing justification mid-paragraph, or
including it in the last line.

## Additional Context

* Introduced in #73
2025-12-21 19:01:00 +11:00
Dave Allie
955c78de64 Book cover sleep screen (#89)
## Summary

* Fix issue with 2-bit bmp rendering
* Add support generate book cover BMP from JPG and use as sleep screen

## Additional Context

* It does not support other image formats beyond JPG at this point
* Something is cooked with my JpegToBmpConverter logic, it generates
weird interlaced looking images for some JPGs

| Book 1 | Book 2|
| --- | --- |
|
![IMG_5653](https://github.com/user-attachments/assets/49bbaeaa-b171-44c7-a68d-14cbe42aef03)
|
![IMG_5652](https://github.com/user-attachments/assets/7db88d70-e09a-49b0-a9a0-4cc729b4ca0c)
|
2025-12-21 18:42:06 +11:00
Dave Allie
958508eb6b Prevent boot loop if last open epub crashes on load (#87)
## Summary

* Unset openEpubPath on boot and set once epub fully loaded

## Additional Context

* If an epub was crashing when loading, it was possible to get the
device stuck into a loop. There was no way to get back to the home
screen as we'd always load you back into old epub
* Break this loop by clearing the stored value when we boot, still
jumping to the last open epub, but only resetting that value once the
epub has been fully loaded
2025-12-21 18:41:52 +11:00
Sam Davis
6aa5d41a42 Add info about sleep screen customisation to user guide (#88)
## Summary

- Updates user guide with information about using custom sleep screens

## Additional Context

N/A
2025-12-21 18:32:50 +11:00
Dave Allie
2a27c6d068 Add JPG image support (#23)
## Summary

- Add basic JPG image support
- Map JPG back to 2-bit BMP output
- Can be used to later render the BMP file from disk or directly pass to
output if wanted
- Give the 3 passes over the data needed to render grayscale content,
putting it on disk is preferred to outputting it multiple times

## Additional Context

- WIP, looking forward to BMP support from
https://github.com/daveallie/crosspoint-reader/pull/16
- Addresses some of #11
2025-12-21 17:15:17 +11:00
Dave Allie
b73ae7fe74 Paginate book list and avoid out of bounds rendering (#86)
## Summary

* Paginate book list
* Avoid out of bounds rendering of long book titles, truncate with
ellipsis instead

## Additional Context

* Should partially help with
https://github.com/daveallie/crosspoint-reader/issues/75 as it was
previously rendering a lot of content off screen, will need to test with
a large directory
2025-12-21 17:12:53 +11:00
Dave Allie
f264efdb12 Extract EPUB TOC into temp file before parsing (#85)
## Summary

* Extract EPUB TOC into temp file before parsing
* Streaming ZIP -> XML parser uses up a lot of memory as we're
allocating inflation buffers while also holding a few copies of the
buffer in different forms
* Instead, but streaming the inflated file down to the SD card (like we
do for HTML parsing, we can lower memory usage)

## Additional Context

* This should help with
https://github.com/daveallie/crosspoint-reader/issues/60 and
https://github.com/daveallie/crosspoint-reader/issues/10. It won't
remove those class of issues completely, but will allow for many more
books to be opened.
2025-12-21 17:08:34 +11:00
Dave Allie
0d32d21d75 Small code cleanup (#83)
## Summary

* Fix cppcheck low violations
* Remove teardown method on parsers, use destructor
* Code cleanup
2025-12-21 15:43:53 +11:00
Dave Allie
9b4dfbd180 Allow any file to be uploaded (#84)
## Summary

- Allow any file to be uploaded
- Removes .epub restriction

## Additional Context

- Fixes #74
2025-12-21 15:43:17 +11:00
Jonas Diemer
926c786705 Keep ZipFile open to speed up getting file stats. (#76)
Still a bit raw, but gets the time required to determine the size of
each chapter (for reading progress) down from ~25ms to 0-1ms.

This is done by keeping the zipArchive open (so simple ;)).

Probably we don't need to cache the spine sizes anymore then...

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-21 14:38:51 +11:00
Dave Allie
299623927e Build out lines when parsing html and holding >750 words in buffer (#73)
## Summary

* Build out lines for pages when holding over 750 buffered words
* Should fix issues with parsing long blocks of text causing memory
crashes
2025-12-21 13:43:19 +11:00
IFAKA
9a3bb81337 fix: add NULL checks after malloc in drawBmp() (#80)
## Problem
`drawBmp()` allocates two row buffers via `malloc()` but doesn't check
if allocations succeed. On low memory, this causes a crash when the NULL
pointers are dereferenced.

## Fix
Add NULL check after both `malloc()` calls. If either fails, log error
and return early.

Changed `lib/GfxRenderer/GfxRenderer.cpp`.

## Test
- Defensive addition only - no logic changes
- Manual device testing appreciated
2025-12-21 13:36:59 +11:00
IFAKA
73d1839ddd fix: add bounds checks to Epub getter functions (#82)
## Problem
Three Epub getter functions can throw exceptions:
- `getCumulativeSpineItemSize()`: No bounds check before
`.at(spineIndex)`
- `getSpineItem()`: If spine is empty and index invalid, `.at(0)` throws
- `getTocItem()`: If toc is empty and index invalid, `.at(0)` throws

## Fix
- Add bounds check to `getCumulativeSpineItemSize()`, return 0 on error
- Add empty container checks to `getSpineItem()` and `getTocItem()`
- Use static fallback objects for safe reference returns on empty
containers

Changed `lib/Epub/Epub.cpp`.

## Test
- Defensive additions - follows existing bounds check patterns
- No logic changes for valid inputs
- Manual device testing appreciated
2025-12-21 13:36:30 +11:00
IFAKA
cc86533e86 fix: add NULL check after malloc in readFileToMemory() (#81)
## Problem
`readFileToMemory()` allocates an output buffer via `malloc()` at line
120 but doesn't check if allocation succeeds. On low memory, the NULL
pointer is passed to `fread()` causing a crash.

## Fix
Add NULL check after `malloc()` for the output buffer. Follows the
existing pattern already used for `deflatedData` at line 141.

Changed `lib/ZipFile/ZipFile.cpp`.

## Test
- Follows existing validated pattern from same function
- Defensive addition only - no logic changes
2025-12-21 13:35:37 +11:00
IFAKA
bf3f270067 fix: add NULL checks for frameBuffer in GfxRenderer (#79)
## Problem
`invertScreen()`, `storeBwBuffer()`, and `restoreBwBuffer()` dereference
`frameBuffer` without NULL validation. If the display isn't initialized,
these functions will crash.

## Fix
Add NULL checks before using `frameBuffer` in all three functions.
Follows the existing pattern from `drawPixel()` (line 11) which already
validates the pointer.

Changed `lib/GfxRenderer/GfxRenderer.cpp`.

## Test
- Follows existing validated pattern from `drawPixel()`
- No logic changes - only adds early return on NULL
- Manual device testing appreciated
2025-12-21 13:34:58 +11:00
Dave Allie
cfe838e03b Update user guide 2025-12-20 01:44:39 +11:00
Dave Allie
7484fe478c Replace cover.jpg 2025-12-20 01:15:20 +11:00
Brendan O'Leary
d41d539435 Add connect to Wifi and File Manager Webserver (#41)
## Summary

- **What is the goal of this PR?**  
Implements wireless EPUB file management via a built-in web server,
enabling users to upload, browse, organize, and delete EPUB files from
any device on the same WiFi network without needing a computer cable
connection.

- **What changes are included?**
- **New Web Server**
([`CrossPointWebServer.cpp`](src/CrossPointWebServer.cpp),
[`CrossPointWebServer.h`](src/CrossPointWebServer.h)):
    - HTTP server on port 80 with a responsive HTML/CSS interface
    - Home page showing device status (version, IP, free memory)
    - File Manager with folder navigation and breadcrumb support
    - EPUB file upload with progress tracking
    - Folder creation and file/folder deletion
    - XSS protection via HTML escaping
- Hidden system folders (`.` prefixed, "System Volume Information",
"XTCache")
  
- **WiFi Screen** ([`WifiScreen.cpp`](src/screens/WifiScreen.cpp),
[`WifiScreen.h`](src/screens/WifiScreen.h)):
    - Network scanning with signal strength indicators
    - Visual indicators for encrypted (`*`) and saved (`+`) networks
- State machine managing: scanning, network selection, password entry,
connecting, save/forget prompts
    - 15-second connection timeout handling
    - Integration with web server (starts on connect, stops on exit)
  
- **WiFi Credential Storage**
([`WifiCredentialStore.cpp`](src/WifiCredentialStore.cpp),
[`WifiCredentialStore.h`](src/WifiCredentialStore.h)):
    - Persistent storage in `/sd/.crosspoint/wifi.bin`
- XOR obfuscation for stored passwords (basic protection against casual
reading)
    - Up to 8 saved networks with add/remove/update operations
  
- **On-Screen Keyboard**
([`OnScreenKeyboard.cpp`](src/screens/OnScreenKeyboard.cpp),
[`OnScreenKeyboard.h`](src/screens/OnScreenKeyboard.h)):
    - Reusable QWERTY keyboard component with shift support
    - Special keys: Shift, Space, Backspace, Done
    - Support for password masking mode
  
- **Settings Screen Integration**
([`SettingsScreen.h`](src/screens/SettingsScreen.h)):
    - Added WiFi action to navigate to the new WiFi screen
  
  - **Documentation** ([`docs/webserver.md`](docs/webserver.md)):
- Comprehensive user guide covering WiFi setup, web interface usage,
file management, troubleshooting, and security notes
    - See this for more screenshots!
- Working "displays the right way in GitHub" on my repo:
https://github.com/olearycrew/crosspoint-reader/blob/feature/connect-to-wifi/docs/webserver.md

**Video demo**


https://github.com/user-attachments/assets/283e32dc-2d9f-4ae2-848e-01f41166a731

## Additional Context

- **Security considerations**: The web server has no
authentication—anyone on the same WiFi network can access files. This is
documented as a limitation, recommending use only on trusted private
networks. Password obfuscation in the credential store is XOR-based, not
cryptographically secure.

- **Memory implications**: The web server and WiFi stack consume
significant memory. The implementation properly cleans up (stops server,
disconnects WiFi, sets `WIFI_OFF` mode) when exiting the WiFi screen to
free resources.

- **Async operations**: Network scanning and connection use async
patterns with FreeRTOS tasks to prevent blocking the UI. The display
task handles rendering on a dedicated thread with mutex protection.

- **Browser compatibility**: The web interface uses standard
HTML5/CSS3/JavaScript and is tested to work with all modern browsers on
desktop and mobile.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-20 01:05:43 +11:00
Dave Allie
cf6fec78dc Cleanup indexing layout string 2025-12-20 00:33:55 +11:00
Jonas Diemer
10d76dde12 Randomly load Sleep Screen from /sleep/*bmp (if exists). (#71)
Load a random sleep screen. 

Only works for ".bmp" (not ".BMP"), but I think that's OK (we do the
same for EPUBs).
2025-12-20 00:17:26 +11:00
Jonas Diemer
7b5a63d220 Option to short-press power button. (#56)
Adresses #53 

Please check if we still need the code to "Give the user up to 1000ms to
start holding the power button, and must hold for
SETTINGS.getPowerButtonDuration()" - the power button should be pressed
already when waking up...

Also, decided to return before the delay to wait more to make the
behavior more immediate.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 23:37:34 +11:00
IFAKA
c1d5f5d562 Add NULL checks after fopen() in ZipFile (#68)
## Problem
Three `fopen()` calls in ZipFile.cpp did not check for NULL before using
the file handle. If files cannot be opened, `fseek`/`fread`/`fclose`
receive NULL and crash.

## Fix
Added NULL checks with appropriate error logging and early returns for
all three locations:
- `getDataOffset()`
- `readFileToMemory()`
- `readFileToStream()`

## Testing
- Builds successfully with `pio run`
- Affects: `lib/ZipFile/ZipFile.cpp`
2025-12-19 23:28:43 +11:00
IFAKA
adfeee063f Handle empty spine in getBookSize() and calculateProgress() (#67)
## Problem
- `getBookSize()` calls `getCumulativeSpineItemSize(getSpineItemsCount()
- 1)` which passes -1 when spine is empty
- `calculateProgress()` then divides by zero when book size is 0

## Fix
- Return 0 from `getBookSize()` if spine is empty
- Return 0 from `calculateProgress()` if book size is 0

## Testing
- Builds successfully with `pio run`
- Affects: `lib/Epub/Epub.cpp`
2025-12-19 23:28:36 +11:00
IFAKA
2d3928ed81 Validate file handle when reading progress.bin (#66)
## Problem
Reading progress.bin used `SD.exists()` then `SD.open()` without
checking if open succeeded. Race conditions or SD errors could cause
file handle to be invalid.

## Fix
- Removed redundant `SD.exists()` check
- Check if file opened successfully before reading
- Verify correct number of bytes were read

## Testing
- Builds successfully with `pio run`
- Affects: `src/activities/reader/EpubReaderActivity.cpp`
2025-12-19 23:27:08 +11:00
IFAKA
48249fbd1e Check SD card initialization and show error on failure (#65)
## Problem
`SD.begin()` return value was ignored. If the SD card fails to
initialize, the device continues and crashes when trying to load
settings/state.

## Fix
Check return value and display "SD card error" message instead of
proceeding with undefined state.

## Testing
- Builds successfully with `pio run`
- Affects: `src/main.cpp`
2025-12-19 23:24:25 +11:00
IFAKA
1a53dccebd Fix title truncation crash for short titles (#63)
## Problem
The status bar title truncation loop crashes when the chapter title is
shorter than 8 characters.

```cpp
// title.length() - 8 underflows when length < 8 (size_t is unsigned)
title = title.substr(0, title.length() - 8) + "...";
```

## Fix
Added a length guard to skip truncation for titles that are too short to
truncate safely.

## Testing
- Builds successfully with `pio run`
- Affects: `src/activities/reader/EpubReaderActivity.cpp`
2025-12-19 23:23:43 +11:00
IFAKA
3e28724b62 Add bounds checking for TOC/spine array access (#64)
## Problem
`getSpineIndexForTocIndex()` and `getTocIndexForSpineIndex()` access
`toc[tocIndex]` and `spine[spineIndex]` without validating indices are
within bounds. Malformed EPUBs or edge cases could trigger out-of-bounds
access.

## Fix
Added bounds validation at the start of both functions before accessing
the arrays.

## Testing
- Builds successfully with `pio run`
- Affects: `lib/Epub/Epub.cpp`
2025-12-19 23:23:23 +11:00
Jonas Diemer
d86b3fe134 Bugfix/word spacing indented (#59)
Simplified the indentation to fix having too large gaps between words
(original calculation was inaccurate).
2025-12-19 08:45:20 +11:00
Dave Allie
1a3d6b125d Custom sleep screen support with BMP reading (#57)
## Summary

* Builds on top of
https://github.com/daveallie/crosspoint-reader/pull/16 - adresses
https://github.com/daveallie/crosspoint-reader/discussions/14
* This PR adds the ability for the user to supply a custom `sleep.bmp`
image at the root of the SD card that will be shown instead of the
default sleep screen if present.
* Supports:
  * Different BPPs:
    * 1bit
    * 2bit
    * 8bit
    * 24bit
    * 32bit (with alpha-channel ignored)
  * Grayscale rendering

---------

Co-authored-by: Sam Davis <sam@sjd.co>
2025-12-19 08:45:14 +11:00
Dave Allie
b2020f5512 Skip pagebreak blocks when parsing epub file (#58)
## Summary

* Skip pagebreak blocks when parsing epub file
* These blocks break the flow and often contain the page number in them
which should not interrupt the flow of the content
- Attributes sourced from:
  - https://www.w3.org/TR/epub-ssv-11/#pagebreak
  - https://www.w3.org/TR/dpub-aria-1.1/#doc-pagebreak
2025-12-19 01:11:03 +11:00
Dave Allie
70dc0f018e Cut release 0.7.0 2025-12-19 00:44:22 +11:00
Jonas Diemer
424594488f Caching of spine item sizes for faster book loading (saves 1-4 seconds). (#54)
As discussed in
https://github.com/daveallie/crosspoint-reader/pull/38#issuecomment-3665142427,
#38
2025-12-18 22:49:14 +11:00
Dave Allie
57fdb1c0fb Rendering "Indexing..." on white screen to avoid partial update 2025-12-18 22:13:24 +11:00
Dave Allie
5e1694748c Fix font readability by expanding blacks and trimming whites (#55)
## Summary

* Previously, only pure black pixels in the font were marked as black,
this expands the black range, and makes the lightest pixels white

## Additional Context

* Noticed personally it was kind of "thin" and washed out a bit, this
massively helps, should also address concerns raised here:
https://github.com/daveallie/crosspoint-reader/discussions/39
2025-12-18 21:39:13 +11:00
Jonas Diemer
063a1df851 Bugfix for #46: don't look at previous chapters if in chapter 0. (#48)
Fixes #46
2025-12-18 06:28:06 +11:00
Dave Allie
d429966dd4 Rename Screens to Activities and restructure files (#44)
## Summary

* This PR drastically reshapes the structure of the codebase, moving
from the concept of "Screens" to "Activities", restructing the files and
setting up the concept of subactivities.
* This should help with keep the main file clean and containing all
functional logic in the relevant activity.
* CrossPointState is now also a global singleton which should help with
accessing it from within activities.

## Additional Context

* This is probably going to be a bit disruptive for people with open
PRs, sorry 😞
2025-12-17 23:32:18 +11:00
Jonas Diemer
c78f2a9840 Calculate the progress in the book by file sizes of each chapter. (#38)
## Summary

Addresses #35.

Maybe it could be wise to do some caching of the spine sizes (but
performance isn't too bad).
2025-12-17 23:05:24 +11:00
Dave Allie
11f01d3a41 Add home screen (#42)
## Summary

* Add home screen
* Sits as new root screen, allows for navigation to settings or file
list
2025-12-17 20:47:43 +11:00
Arthur Tazhitdinov
973d372521 TOC location fix (#25)
## Summary

* Rely on media-type="application/x-dtbncx+xml" to find TOC instead of
hardcoded values

## Additional Context

* Most of my epubs don't have id==ncx for toc file location. I think
this media-type is EPUB standard

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-17 18:49:45 +11:00
Dave Allie
67da8139b3 Use 6x8kB chunks instead of 1x48kB chunk for secondary display buffer (#36)
## Summary

- When allocating the `bwBuffer` required to restore the RED RAM in the
EPD, we were previously allocating the whole frame buffer in one
contiguous memory chunk (48kB)
- Depending on the state of memory fragmentation at the time of this
call, it may not be possible to allocate all that memory
- Instead, we now allocate 6 blocks of 8kB instead of the full 48kB,
this should mean the display updates are more resilient to different
memory conditions

## Additional Context
2025-12-17 01:39:22 +11:00
Dave Allie
c287aa03a4 Use single buffer mode for EInkDisplay (#34)
## Summary

* Frees up 48kB of statically allocated RAM in exchange for 48kB just
when grayscale rendering is needed

## Additional Context

* Upstream changes:
https://github.com/open-x4-epaper/community-sdk/pull/7
2025-12-17 00:17:49 +11:00
Dave Allie
5d68c8b305 Cut release 0.6.0 2025-12-16 23:15:47 +11:00
Jonas Diemer
def7abbd60 Improve indent (#28)
* add horizontal indent in first line of paragraph in case Extra Paragraph Spacing is OFF

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

* Changed size of indent to 1 em.

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

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

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

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

* add cyrillic ranges

* revert

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

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

* quicker pwr button

* no extra spacing between paragraphs

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

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

* Added setting for extra paragraph spacing.

* fixed typo

* Rework after feedback

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

* Checkout submodules

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

* Cleanup ParsedText

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

* Bump Page and Section file versions

* Combine removeDir implementations in Epub

* Adjust screen margins
2025-12-12 22:13:34 +11:00
Dave Allie
09f68a3d03 Restructure readme 2025-12-09 00:07:38 +11:00
Dave Allie
7ec7efcb47 Add section in readme on flashing via xteink.dve.al 2025-12-09 00:04:55 +11:00
Dave Allie
45af2d0e81 Cut release 0.3.0 2025-12-08 23:55:05 +11:00
Dave Allie
0926e9e6e4 Add version string to boot screen 2025-12-08 23:13:33 +11:00
Dave Allie
02b157c02b Add drawCenteredText to GfxRenderer 2025-12-08 22:52:19 +11:00
Dave Allie
07cc589e59 Cleanup serial output 2025-12-08 22:39:23 +11:00
Dave Allie
b743a1ca8e Remove EpdRenderer and create new GfxRenderer 2025-12-08 22:06:09 +11:00
Dave Allie
2ed8017aa2 Move to SDK EInkDisplay and enable anti-aliased 2-bit text (#5)
* First pass at moving to SDK EInkDisplay library

* Add 2-bit grayscale text and anti-aliased rendering of text

* Render status bar for empty chapters

* Refresh screen every 15 pages to avoid ghosting

* Simplify boot and sleep screens

* Give FileSelectionScreen task more stack memory

* Move text around slightly on Boot and Sleep screens

* Re-use existing buffer and write to whole screen for 'partial update'
2025-12-08 19:48:49 +11:00
278 changed files with 322818 additions and 13958 deletions

2
.clangd Normal file
View File

@@ -0,0 +1,2 @@
CompileFlags:
Add: [-std=c++2a]

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [daveallie]
ko_fi: daveallie

54
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Bug Report
description: Report an issue or unexpected behavior
title: "Short, descriptive title of the issue"
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report this bug! Please fill out the details below.
- type: input
id: version
attributes:
label: Affected Version
description: What version of the project/library are you using? (e.g., v1.2.3, master branch commit SHA)
placeholder: Ex. v1.2.3
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the Bug
description: A clear and concise description of what the bug is.
placeholder:
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Clearly list the steps necessary to reproduce the unexpected behavior.
placeholder: |
1. Go to '...'
2. Select '...'
3. Crash
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Log Output/Screenshots
description: If applicable, error messages, or log output to help explain your problem. You can drag and drop images here.
render: shell

18
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,18 @@
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO >**_

37
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: CI
'on':
push:
branches: [master]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Install clang-format-21
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 21
sudo apt-get update
sudo apt-get install -y clang-format-21
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
- name: Build CrossPoint
run: pio run

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 }}

41
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Compile Release
on:
push:
tags:
- '*'
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Build CrossPoint
run: pio run -e gh_release
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: CrossPoint-${{ github.ref_name }}
path: |
.pio/build/gh_release/bootloader.bin
.pio/build/gh_release/firmware.bin
.pio/build/gh_release/firmware.elf
.pio/build/gh_release/firmware.map
.pio/build/gh_release/partitions.bin

8
.gitignore vendored
View File

@@ -1,3 +1,11 @@
.pio
.idea
.DS_Store
.vscode
lib/EpdFont/fontsrc
*.generated.h
.vs
build
**/__pycache__/
/compile_commands.json
/.cache

View File

@@ -6,7 +6,7 @@ Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
![](./docs/cover.jpg)
![](./docs/images/cover.jpg)
## Motivation
@@ -23,20 +23,52 @@ CrossPoint Reader aims to:
This project is **not affiliated with Xteink**; it's built as a community project.
## Features
## Features & Usage
- [x] EPUB parsing and rendering
- [x] EPUB parsing and rendering (EPUB 2 and EPUB 3)
- [ ] Image support within EPUB
- [x] Saved reading position
- [ ] File explorer with file picker
- [x] File explorer with file picker
- [x] Basic EPUB picker from root directory
- [x] Support nested folders
- [ ] EPUB picker with cover art
- [ ] Image support within EPUB
- [ ] Configurable font, layout, and display options
- [ ] WiFi connectivity
- [ ] BLE connectivity
- [x] Custom sleep screen
- [x] Cover sleep screen
- [x] Wifi book upload
- [x] Wifi OTA updates
- [x] Configurable font, layout, and display options
- [ ] User provided fonts
- [ ] Full UTF support
- [x] Screen rotation
## Getting Started
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
## Installing
### Web (latest firmware)
1. Connect your Xteink X4 to your computer via USB-C
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Web (specific firmware version)
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Manual
See [Development](#development) below.
## Development
### Prerequisites
@@ -58,33 +90,21 @@ git submodule update --init --recursive
### Flashing your device
#### Command line
Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
pio run --target upload
```
#### Web
1. Connect your Xteink X4 to your computer via USB-C
2. Download the `firmware.bin` file from the latest release via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
4. Press the reset button on the Xteink X4 to restart the device
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
## Internals
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
on this constraint.
### EPUB caching
### Data caching
The first time chapters of an EPUB are loaded, they are cached to the SD card. Subsequent loads are served from the
The first time chapters of a book are loaded, they are cached to the SD card. Subsequent loads are served from the
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
@@ -92,30 +112,30 @@ cache. This cache directory exists at `.crosspoint` on the SD card. The structur
.crosspoint/
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
│ ├── 0/ # Each chapter is stored in a subdirectory named by its index (based on the spine order)
│ ├── section.bin # Section metadata (page count)
│ ├── page_0.bin # Each page is stored in a separate file, it
├── page_1.bin # contains the position (x, y) and text for each word
── ...
├── 1/
│ │ ├── section.bin
│ │ ├── page_0.bin
│ │ ├── page_1.bin
│ │ └── ...
│ └── ...
│ ├── cover.bmp # Book cover image (once generated)
│ ├── book.bin # Book metadata (title, author, spine, table of contents, etc.)
└── sections/ # All chapter data is stored in the sections subdirectory
├── 0.bin # Chapter data (screen count, all text layout info, etc.)
── 1.bin # files are named by their index in the spine
└── ...
└── epub_189013891/
```
Deleting the `.crosspoint` directory will clear the cache.
Deleting the `.crosspoint` directory will clear the entire cache.
Due the way it's currently implemented, the cache is not automatically cleared when the EPUB is deleted and moving an
EPUB file will reset the reading progress.
Due the way it's currently implemented, the cache is not automatically cleared when a book is deleted and moving a book
file will use a new cache directory, resetting the reading progress.
For more details on the internal file structures, see the [file formats document](./docs/file-formats.md).
## Contributing
Contributions are very welcome!
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/daveallie/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
### To submit a contribution:
1. Fork the repo

222
USER_GUIDE.md Normal file
View File

@@ -0,0 +1,222 @@
# CrossPoint User Guide
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of the device.
- [CrossPoint User Guide](#crosspoint-user-guide)
- [1. Hardware Overview](#1-hardware-overview)
- [Button Layout](#button-layout)
- [2. Power \& Startup](#2-power--startup)
- [Power On / Off](#power-on--off)
- [First Launch](#first-launch)
- [3. Screens](#3-screens)
- [3.1 Home Screen](#31-home-screen)
- [3.2 Book Selection](#32-book-selection)
- [3.3 Reading Mode](#33-reading-mode)
- [3.4 File Upload Screen](#34-file-upload-screen)
- [3.5 Settings](#35-settings)
- [3.6 Sleep Screen](#36-sleep-screen)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [System Navigation](#system-navigation)
- [5. Chapter Selection Screen](#5-chapter-selection-screen)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
## 1. Hardware Overview
The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default):
### Button Layout
| Location | Buttons |
| --------------- | ---------------------------------------------------- |
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down**, **Reset** |
Button layout can be customized in **[Settings](#35-settings)**.
---
## 2. Power & Startup
### Power On / Off
To turn the device on or off, **press and hold the Power button for approximately half a second**.
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
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
Upon turning the device on for the first time, you will be placed on the **[Home](#31-home-screen)** screen.
> [!NOTE]
> On subsequent restarts, the firmware will automatically reopen the last book you were reading.
---
## 3. Screens
### 3.1 Home Screen
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
### 3.2 Book Selection
The Book Selection acts as a folder and file browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
### 3.3 Reading Mode
See [Reading Mode](#4-reading-mode) below for more information.
### 3.4 File Upload Screen
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
> [!TIP]
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
### 3.4.1 Calibre Wireless Transfers
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
1. Install the plugin in Calibre:
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
- Download the zip file.
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
2. On the device: File Transfer → Connect to Calibre → Join a network.
3. Make sure your computer is on the same WiFi network.
4. In Calibre, click "Send to device" to transfer books.
### 3.5 Settings
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
- "Dark" (default) - The default dark Crosspoint logo sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
- "Full" - Show status bar with reading progress
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default)
- "In Reader" - Show battery percentage everywhere except in reading mode
- "Always" - Always hide battery percentage
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" - Require a long press to turn off the device
- "Sleep" - A short press powers the device off
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
- "Portrait" (default) - Standard portrait orientation
- "Landscape CW" - Landscape, rotated clockwise
- "Inverted" - Portrait, upside down
- "Landscape CCW" - Landscape, rotated counter-clockwise
- **Front Button Layout**: Configure the order of the bottom edge buttons:
- Back, Confirm, Left, Right (default)
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- Back, Confirm, Right, Left
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
- **Check for updates**: Check for firmware updates over WiFi.
### 3.6 Sleep Screen
You can customize the sleep screen by placing custom images in specific locations on the SD card:
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
> [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
> [!TIP]
> For best results:
> - Use uncompressed BMP files with 24-bit color depth
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
---
## 4. Reading Mode
Once you have opened a book, the button layout changes to facilitate reading.
### Page Turning
| Action | Buttons |
| ----------------- | ------------------------------------ |
| **Previous Page** | Press **Left** _or_ **Volume Up** |
| **Next Page** | Press **Right** _or_ **Volume Down** |
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
### Chapter Navigation
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
### Supported Languages
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
---
## 5. Chapter Selection Screen
Accessible by pressing **Confirm** while inside a book.
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
2. Press **Confirm** to jump to that chapter.
3. *Alternatively, press **Back** to cancel and return to your current page.*
---
## 6. Current Limitations & Roadmap
Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates:
* **Images:** Embedded images in e-books will not render.

View File

@@ -1,3 +1,20 @@
#!/bin/bash
find src lib \( -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) -exec clang-format -style=file -i {} +
GIT_LS_FILES_FLAGS=""
if [[ "$1" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified"
fi
# --- Main Logic ---
# Format all files (or only modified files if -g is passed)
# Use 'git ls-files' to get a list of all files tracked by git:
# --modified: files tracked by git that have been modified (staged or unstaged)
# --exclude-standard: ignores files in .gitignore
# Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated.
git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
| grep -E '\.(c|cpp|h|hpp)$' \
| grep -v -E '^lib/CrossPointFont/builtinFonts/' \
| grep -v -E '^lib/CrossPointFont/Group5' \
| xargs -r clang-format -style=file -i

19
docs/comparison.md Normal file
View File

@@ -0,0 +1,19 @@
# CrossPoint vs XTOS
Below is like for like comparison of CrossPoint (version 0.5.1) and XTOS (version 3.1.1). CrossPoint is on the left,
XTOS is on the right. CrossPoint does not currently support all features of XTOS, so this comparison is just of key
features which both firmwares support.
## EPUB reading
![](./images/comparison/reading-1.jpg)
![](./images/comparison/reading-2.jpg)
![](./images/comparison/reading-3.jpg)
## Menus
![](./images/comparison/menu.jpg)
![](./images/comparison/chapter-menu.jpg)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

221
docs/file-formats.md Normal file
View File

@@ -0,0 +1,221 @@
# File Formats
## `book.bin`
### Version 3
ImHex Pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 3
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
std::warning(std::format("Unusually large string length: {} bytes", length));
}
char data[length] [[comment("UTF-8 string data")]];
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
fn format_string(String s) {
return s.data;
};
// === Metadata Structure ===
struct Metadata {
String title [[comment("Book title")]];
String author [[comment("Book author")]];
String coverItemHref [[comment("Path to cover image")]];
String textReferenceHref [[comment("Path to guided first text reference")]];
} [[comment("Book metadata information")]];
// === Spine Entry Structure ===
struct SpineEntry {
String href [[comment("Resource path")]];
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
} [[comment("Spine entry defining reading order")]];
// === TOC Entry Structure ===
struct TocEntry {
String title [[comment("Chapter/section title")]];
String href [[comment("Resource path")]];
String anchor [[comment("Fragment identifier")]];
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
} [[comment("Table of contents entry")]];
// === Book Bin Structure ===
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
};
// === File Parsing ===
BookBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
```
## `section.bin`
### Version 8
ImHex Pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 8
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
std::warning(std::format("Unusually large string length: {} bytes", length));
}
char data[length] [[comment("UTF-8 string data")]];
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
fn format_string(String s) {
return s.data;
};
// === Page Structure ===
enum StorageType : u8 {
PageLine = 1
};
enum WordStyle : u8 {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3
};
enum BlockStyle : u8 {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
struct PageLine {
s16 xPos;
s16 yPos;
u16 wordCount;
String words[wordCount];
u16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
BlockStyle blockStyle;
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageLine pageLine [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
};
struct Page {
u16 elementCount;
PageElement elements[elementCount] [[inline]];
};
// === Section Bin Structure ===
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
bool extraParagraphSpacing;
u16 viewportWidth;
u16 vieportHeight;
u16 pageCount;
u32 lutOffset;
Page page[pageCount];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 lut[pageCount];
};
// === File Parsing ===
SectionBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
```

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
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
docs/images/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

57
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,57 @@
# Troubleshooting
This document show most common issues and possible solutions while using the device features.
- [Troubleshooting](#troubleshooting)
- [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network)
- [Connection Drops or Times Out](#connection-drops-or-times-out)
- [Upload Fails](#upload-fails)
- [Saved Password Not Working](#saved-password-not-working)
### Cannot See the Device on the Network
**Problem:** Browser shows "Cannot connect" or "Site can't be reached"
**Solutions:**
1. Verify both devices are on the **same WiFi network**
- Check your computer/phone WiFi settings
- Confirm the CrossPoint Reader shows "Connected" status
2. Double-check the IP address
- Make sure you typed it correctly
- Include `http://` at the beginning
3. Try disabling VPN if you're using one
4. Some networks have "client isolation" enabled - check with your network administrator
### Connection Drops or Times Out
**Problem:** WiFi connection is unstable
**Solutions:**
1. Move closer to the WiFi router
2. Check signal strength on the device (should be at least `||` or better)
3. Avoid interference from other devices
4. Try a different WiFi network if available
### Upload Fails
**Problem:** File upload doesn't complete or shows an error
**Solutions:**
1. Ensure the file is a valid `.epub` file
2. Check that the SD card has enough free space
3. Try uploading a smaller file first to test
4. Refresh the browser page and try again
### Saved Password Not Working
**Problem:** Device fails to connect with saved credentials
**Solutions:**
1. When connection fails, you'll be prompted to "Forget Network"
2. Select **Yes** to remove the saved password
3. Reconnect and enter the password again
4. Choose to save the new password

331
docs/webserver-endpoints.md Normal file
View File

@@ -0,0 +1,331 @@
# Webserver Endpoints
This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver.
- [Webserver Endpoints](#webserver-endpoints)
- [Overview](#overview)
- [HTTP Endpoints](#http-endpoints)
- [GET `/` - Home Page](#get----home-page)
- [GET `/files` - File Browser Page](#get-files---file-browser-page)
- [GET `/api/status` - Device Status](#get-apistatus---device-status)
- [GET `/api/files` - List Files](#get-apifiles---list-files)
- [POST `/upload` - Upload File](#post-upload---upload-file)
- [POST `/mkdir` - Create Folder](#post-mkdir---create-folder)
- [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder)
- [WebSocket Endpoint](#websocket-endpoint)
- [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload)
- [Network Modes](#network-modes)
- [Station Mode (STA)](#station-mode-sta)
- [Access Point Mode (AP)](#access-point-mode-ap)
- [Notes](#notes)
## Overview
The CrossPoint Reader exposes a webserver for file management and device monitoring:
- **HTTP Server**: Port 80
- **WebSocket Server**: Port 81 (for fast binary uploads)
---
## HTTP Endpoints
### GET `/` - Home Page
Serves the home page HTML interface.
**Request:**
```bash
curl http://crosspoint.local/
```
**Response:** HTML page (200 OK)
---
### GET `/files` - File Browser Page
Serves the file browser HTML interface.
**Request:**
```bash
curl http://crosspoint.local/files
```
**Response:** HTML page (200 OK)
---
### GET `/api/status` - Device Status
Returns JSON with device status information.
**Request:**
```bash
curl http://crosspoint.local/api/status
```
**Response (200 OK):**
```json
{
"version": "1.0.0",
"ip": "192.168.1.100",
"mode": "STA",
"rssi": -45,
"freeHeap": 123456,
"uptime": 3600
}
```
| Field | Type | Description |
| ---------- | ------ | --------------------------------------------------------- |
| `version` | string | CrossPoint firmware version |
| `ip` | string | Device IP address |
| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) |
| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) |
| `freeHeap` | number | Free heap memory in bytes |
| `uptime` | number | Seconds since device boot |
---
### GET `/api/files` - List Files
Returns a JSON array of files and folders in the specified directory.
**Request:**
```bash
# List root directory
curl http://crosspoint.local/api/files
# List specific directory
curl "http://crosspoint.local/api/files?path=/Books"
```
**Query Parameters:**
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ---------------------- |
| `path` | No | `/` | Directory path to list |
**Response (200 OK):**
```json
[
{"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true},
{"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false},
{"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false}
]
```
| Field | Type | Description |
| ------------- | ------- | ---------------------------------------- |
| `name` | string | File or folder name |
| `size` | number | Size in bytes (0 for directories) |
| `isDirectory` | boolean | `true` if the item is a folder |
| `isEpub` | boolean | `true` if the file has `.epub` extension |
**Notes:**
- Hidden files (starting with `.`) are automatically filtered out
- System folders (`System Volume Information`, `XTCache`) are hidden
---
### POST `/upload` - Upload File
Uploads a file to the SD card via multipart form data.
**Request:**
```bash
# Upload to root directory
curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload
# Upload to specific directory
curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books"
```
**Query Parameters:**
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ------------------------------- |
| `path` | No | `/` | Target directory for the upload |
**Response (200 OK):**
```
File uploaded successfully: mybook.epub
```
**Error Responses:**
| Status | Body | Cause |
| ------ | ----------------------------------------------- | --------------------------- |
| 400 | `Failed to create file on SD card` | Cannot create file |
| 400 | `Failed to write to SD card - disk may be full` | Write error during upload |
| 400 | `Failed to write final data to SD card` | Error flushing final buffer |
| 400 | `Upload aborted` | Client aborted the upload |
| 400 | `Unknown error during upload` | Unspecified error |
**Notes:**
- Existing files with the same name will be overwritten
- Uses a 4KB buffer for efficient SD card writes
---
### POST `/mkdir` - Create Folder
Creates a new folder on the SD card.
**Request:**
```bash
curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir
```
**Form Parameters:**
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | ---------------------------- |
| `name` | Yes | - | Name of the folder to create |
| `path` | No | `/` | Parent directory path |
**Response (200 OK):**
```
Folder created: NewFolder
```
**Error Responses:**
| Status | Body | Cause |
| ------ | ----------------------------- | ----------------------------- |
| 400 | `Missing folder name` | `name` parameter not provided |
| 400 | `Folder name cannot be empty` | Empty folder name |
| 400 | `Folder already exists` | Folder with same name exists |
| 500 | `Failed to create folder` | SD card error |
---
### POST `/delete` - Delete File or Folder
Deletes a file or folder from the SD card.
**Request:**
```bash
# Delete a file
curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete
# Delete an empty folder
curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete
```
**Form Parameters:**
| Parameter | Required | Default | Description |
| --------- | -------- | ------- | -------------------------------- |
| `path` | Yes | - | Path to the item to delete |
| `type` | No | `file` | Type of item: `file` or `folder` |
**Response (200 OK):**
```
Deleted successfully
```
**Error Responses:**
| Status | Body | Cause |
| ------ | --------------------------------------------- | ----------------------------- |
| 400 | `Missing path` | `path` parameter not provided |
| 400 | `Cannot delete root directory` | Attempted to delete `/` |
| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder |
| 403 | `Cannot delete system files` | Hidden file (starts with `.`) |
| 403 | `Cannot delete protected items` | Protected system folder |
| 404 | `Item not found` | Path does not exist |
| 500 | `Failed to delete item` | SD card error |
**Protected Items:**
- Files/folders starting with `.`
- `System Volume Information`
- `XTCache`
---
## WebSocket Endpoint
### Port 81 - Fast Binary Upload
A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files.
**Connection:**
```
ws://crosspoint.local:81/
```
**Protocol:**
1. **Client** sends TEXT message: `START:<filename>:<size>:<path>`
2. **Server** responds with TEXT: `READY`
3. **Client** sends BINARY messages with file data chunks
4. **Server** sends TEXT progress updates: `PROGRESS:<received>:<total>`
5. **Server** sends TEXT when complete: `DONE` or `ERROR:<message>`
**Example Session:**
```
Client -> "START:mybook.epub:1234567:/Books"
Server -> "READY"
Client -> [binary chunk 1]
Client -> [binary chunk 2]
Server -> "PROGRESS:65536:1234567"
Client -> [binary chunk 3]
...
Server -> "PROGRESS:1234567:1234567"
Server -> "DONE"
```
**Error Messages:**
| Message | Cause |
| --------------------------------- | ---------------------------------- |
| `ERROR:Failed to create file` | Cannot create file on SD card |
| `ERROR:Invalid START format` | Malformed START message |
| `ERROR:No upload in progress` | Binary data received without START |
| `ERROR:Write failed - disk full?` | SD card write error |
**Example with `websocat`:**
```bash
# Interactive session
websocat ws://crosspoint.local:81
# Then type:
START:mybook.epub:1234567:/Books
# Wait for READY, then send binary data
```
**Notes:**
- Progress updates are sent every 64KB or at completion
- Disconnection during upload will delete the incomplete file
- Existing files with the same name will be overwritten
---
## Network Modes
The device can operate in two network modes:
### Station Mode (STA)
- Device connects to an existing WiFi network
- IP address assigned by router/DHCP
- `mode` field in `/api/status` returns `"STA"`
- `rssi` field shows signal strength
### Access Point Mode (AP)
- Device creates its own WiFi hotspot
- Default IP is typically `192.168.4.1`
- `mode` field in `/api/status` returns `"AP"`
- `rssi` field returns `0`
---
## Notes
- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`).
- All paths on the SD card start with `/`
- Trailing slashes are automatically stripped (except for root `/`)
- The webserver uses chunked transfer encoding for file listings

225
docs/webserver.md Normal file
View File

@@ -0,0 +1,225 @@
# Web Server Guide
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone.
## Overview
CrossPoint Reader includes a built-in web server that allows you to:
- Upload EPUB files wirelessly from any device on the same WiFi network
- Browse and manage files on your device's SD card
- Create folders to organize your ebooks
- Delete files and folders
## Prerequisites
- Your CrossPoint Reader device
- A WiFi network
- A computer, phone, or tablet connected to the **same WiFi network**
---
## Step 1: Accessing the WiFi Screen
1. From the main menu or file browser, navigate to the **Settings** screen
2. Select the **WiFi** option
3. The device will automatically start scanning for available networks
---
## Step 2: Connecting to WiFi
### Viewing Available Networks
Once the scan completes, you'll see a list of available WiFi networks with the following indicators:
- **Signal strength bars** (`||||`, `|||`, `||`, `|`) - Shows connection quality
- **`*` symbol** - Indicates the network is password-protected (encrypted)
- **`+` symbol** - Indicates you have previously saved credentials for this network
<img src="./images/wifi/wifi_networks.jpeg" height="500">
### Selecting a Network
1. Use the **Left/Right** (or **Volume Up/Down**) buttons to navigate through the network list
2. Press **Confirm** to select the highlighted network
### Entering Password (for encrypted networks)
If the network requires a password:
1. An on-screen keyboard will appear
2. Use the navigation buttons to select characters
3. Press **Confirm** to enter each character
4. When complete, select the **Done** option on the keyboard
<img src="./images/wifi/wifi_password.jpeg" height="500">
**Note:** If you've previously connected to this network, the saved password will be used automatically.
### Connection Process
The device will display "Connecting..." while establishing the connection. This typically takes 5-10 seconds.
### Saving Credentials
If this is a new network, you'll be prompted to save the password:
- Select **Yes** to save credentials for automatic connection next time (NOTE: These are stored in plaintext on the device's SD card. Do not use this for sensitive networks.)
- Select **No** to connect without saving
---
## Step 3: Connection Success
Once connected, the screen will display:
- **Network name** (SSID)
- **IP Address** (e.g., `192.168.1.102`)
- **Web server URL** (e.g., `http://192.168.1.102/`)
<img src="./images/wifi/wifi_connected.jpeg" height="500">
**Important:** Make note of the IP address - you'll need this to access the web interface from your computer or phone.
---
## Step 4: Accessing the Web Interface
### From a Computer
1. Ensure your computer is connected to the **same WiFi network** as your CrossPoint Reader
2. Open any web browser (Chrome is recommended)
3. Type the IP address shown on your device into the browser's address bar
- Example: `http://192.168.1.102/`
4. Press Enter
### From a Phone or Tablet
1. Ensure your phone/tablet is connected to the **same WiFi network** as your CrossPoint Reader
2. Open your mobile browser (Safari, Chrome, etc.)
3. Type the IP address into the address bar
- Example: `http://192.168.1.102/`
4. Tap Go
---
## Step 5: Using the Web Interface
### Home Page
The home page displays:
- Device status and version information
- WiFi connection status
- Current IP address
- Available memory
Navigation links:
- **Home** - Returns to the status page
- **File Manager** - Access file management features
<img src="./images/wifi/webserver_homepage.png" width="600">
### File Manager
Click **File Manager** to access file management features.
#### Browsing Files
- The file manager displays all files and folders on your SD card
- **Folders** are highlighted in yellow with a 📁 icon
- **EPUB files** are highlighted in green with a 📗 icon
- Click on a folder name to navigate into it
- Use the breadcrumb navigation at the top to go back to parent folders
<img src="./images/wifi/webserver_files.png" width="600">
#### Uploading EPUB Files
1. Click the **+ Add** button in the top-right corner
2. Select **Upload eBook** from the dropdown menu
3. Click **Choose File** and select an `.epub` file from your device
4. Click **Upload**
5. A progress bar will show the upload status
6. The page will automatically refresh when the upload is complete
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
<img src="./images/wifi/webserver_upload.png" width="600">
#### Creating Folders
1. Click the **+ Add** button in the top-right corner
2. Select **New Folder** from the dropdown menu
3. Enter a folder name (letters, numbers, underscores, and hyphens only)
4. Click **Create Folder**
This is useful for organizing your ebooks by genre, author, or series.
#### Deleting Files and Folders
1. Click the **🗑️** (trash) icon next to any file or folder
2. Confirm the deletion in the popup dialog
3. Click **Delete** to permanently remove the item
**Warning:** Deletion is permanent and cannot be undone!
**Note:** Folders must be empty before they can be deleted.
---
## Command Line File Management
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md).
## Security Notes
- The web server runs on port 80 (standard HTTP)
- **No authentication is required** - anyone on the same network can access the interface
- The web server is only accessible while the WiFi screen shows "Connected"
- The web server automatically stops when you exit the WiFi screen
- For security, only use on trusted private networks
---
## Technical Details
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
- **Web Server Port:** 80 (HTTP)
- **Maximum Upload Size:** Limited by available SD card space
- **Supported File Format:** `.epub` only
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
---
## Tips and Best Practices
1. **Organize with folders** - Create folders before uploading to keep your library organized
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery
---
## Exiting WiFi Mode
When you're finished uploading files:
1. Press the **Back** button on your CrossPoint Reader
2. The web server will automatically stop
3. WiFi will disconnect to conserve battery
4. You'll return to the previous screen
Your uploaded files will be immediately available in the file browser!
---
## Related Documentation
- [User Guide](../USER_GUIDE.md) - General device operation
- [Troubleshooting](./troubleshooting.md) - Troubleshooting
- [README](../README.md) - Project overview and features

View File

@@ -0,0 +1,147 @@
#include "CrossPointFont.h"
#include <Utf8.h>
#include <algorithm>
#include <cmath>
#define FONT_SCALE 2
namespace {
// Number of set bits from 0->15
uint8_t bitCount[] = {
0, // 0b0000,
1, // 0b0001,
1, // 0b0010,
2, // 0b0011,
1, // 0b0100,
2, // 0b0101,
2, // 0b0110,
3, // 0b0111,
1, // 0b1000,
2, // 0b1001,
2, // 0b1010,
3, // 0b1011,
2, // 0b1100,
3, // 0b1101,
3, // 0b1110,
4, // 0b1111,
};
} // namespace
void CrossPointFont::getTextBounds(const char* string, const Style style, const int startX, const int startY, int* minX,
int* minY, int* maxX, int* maxY) const {
*minX = startX;
*minY = startY;
*maxX = startX;
*maxY = startY;
if (*string == '\0') {
return;
}
int cursorX = startX;
const int cursorY = startY;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const CrossPointFontGlyph* glyph = getGlyph(cp, style);
if (!glyph) {
glyph = getGlyph(REPLACEMENT_GLYPH, style);
}
if (!glyph) {
// TODO: Better handle this?
continue;
}
*minX = std::min(*minX, cursorX + glyph->xOffset / FONT_SCALE);
*maxX = std::max(*maxX, cursorX + (glyph->xOffset + glyph->width) / FONT_SCALE);
*minY = std::min(*minY, cursorY + (glyph->yOffset - glyph->height) / FONT_SCALE);
*maxY = std::max(*maxY, cursorY + glyph->yOffset / FONT_SCALE);
cursorX += glyph->xAdvance / FONT_SCALE;
}
}
void CrossPointFont::getTextDimensions(const char* string, const Style style, int* w, int* h) const {
int minX = 0, minY = 0, maxX = 0, maxY = 0;
getTextBounds(string, style, 0, 0, &minX, &minY, &maxX, &maxY);
*w = maxX - minX;
*h = maxY - minY;
}
uint8_t CrossPointFont::styleGroup(const Style style) const {
if (style == REGULAR) return 0;
if (data.header.styles == 0b0001) {
// Only have regular font, show regular
return 0;
}
if (data.header.styles == 0b0011) {
// Only have bold and regular font
// Show bold if style is bold or bold_italic
return style == BOLD || style == BOLD_ITALIC ? 1 : 0;
}
if (data.header.styles == 0b0101) {
// Only have italic and regular font
// Show italic if style is italic or bold_italic
return style == ITALIC || style == BOLD_ITALIC ? 1 : 0;
}
if (data.header.styles == 0b1001) {
// Only have bold_italic and regular font
// Show bold_italic if style is any non-regular
return style == BOLD_ITALIC || style == BOLD || style == ITALIC ? 1 : 0;
}
if (data.header.styles == 0b0111) {
// Have all but bold_italic
// Show bold if style is bold_italic, otherwise show the requested style
return style == BOLD_ITALIC ? 1 : style;
}
if (data.header.styles == 0b1011) {
// Have all but italic
// Show bold_italic if style is italic, otherwise show the requested style
return style == ITALIC ? 2 : style;
}
if (data.header.styles == 0b1101) {
// Have all but bold
// Show bold_italic if style is bold, otherwise show the requested style
return style == BOLD ? 2 : style;
}
if (data.header.styles == 0b1111) {
return style;
}
return 0;
}
const CrossPointFontGlyph* CrossPointFont::getGlyph(const uint32_t cp, const Style style) const {
const CrossPointFontUnicodeInterval* intervals = data.intervals;
const int count = data.header.intervalCount;
if (count == 0) return nullptr;
// Binary search for O(log n) lookup instead of O(n)
// Critical for Korean fonts with many unicode intervals
int left = 0;
int right = count - 1;
while (left <= right) {
const int mid = left + (right - left) / 2;
const CrossPointFontUnicodeInterval* interval = &intervals[mid];
if (cp < interval->first) {
right = mid - 1;
} else if (cp > interval->last) {
left = mid + 1;
} else {
// Found: cp >= interval->first && cp <= interval->last
const uint32_t index =
interval->offset + (cp - interval->first) * bitCount[data.header.styles] + styleGroup(style);
return &data.glyphs[index];
}
}
return nullptr;
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include "CrossPointFontFormat.h"
#include "Group5/Group5.h"
class CrossPointFont {
public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
CrossPointFontData data;
explicit CrossPointFont(void* rawData) {
data.header = *static_cast<CrossPointFontHeader*>(rawData);
data.intervals = (CrossPointFontUnicodeInterval*)(static_cast<uint8_t*>(rawData) + sizeof(CrossPointFontHeader));
data.glyphs = (CrossPointFontGlyph*)((uint8_t*)data.intervals +
sizeof(CrossPointFontUnicodeInterval) * data.header.intervalCount);
data.bitmap = (uint8_t*)data.glyphs + sizeof(CrossPointFontGlyph) * data.header.glyphCount;
}
~CrossPointFont() = default;
void getTextDimensions(const char* string, Style style, int* w, int* h) const;
const CrossPointFontGlyph* getGlyph(uint32_t cp, Style style) const;
private:
void getTextBounds(const char* string, Style style, int startX, int startY, int* minX, int* minY, int* maxX,
int* maxY) const;
uint8_t styleGroup(Style style) const;
};
// TODO: CrossPointFontSmall

View File

@@ -0,0 +1,57 @@
#pragma once
// 16-bit marker at the start of a CrossPoint font file
// (CrossPoint Font Format)
#define CPF_FONT_MARKER 0xCFF1
#define CPF_FONT_MARKER_SMALL 0xCFF2
// Font info per large character (glyph)
typedef struct {
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
uint16_t width; /// bitmap width in pixels
uint16_t height; /// bitmap height in pixels
uint16_t xAdvance; /// total width in pixels (bitmap + padding)
int16_t xOffset; /// left padding to upper left corner
int16_t yOffset; /// top padding to upper left corner
} CrossPointFontGlyph;
// Font info per small character (glyph)
typedef struct {
uint32_t bitmapOffset; /// Offset to compressed bitmap data for this glyph
uint8_t width; /// bitmap width in pixels
uint8_t height; /// bitmap height in pixels
uint8_t xAdvance; /// total width in pixels (bitmap + padding)
int8_t xOffset; /// left padding to upper left corner
int16_t yOffset; /// top padding to upper left corner
} CrossPointFontSmallGlyph;
/// Glyph interval structure
typedef struct {
uint32_t first; /// The first unicode code point of the interval
uint32_t last; /// The last unicode code point of the interval
uint32_t offset; /// Index of the first code point into the glyph array
} CrossPointFontUnicodeInterval;
typedef struct {
uint16_t u16Marker; /// CPF_FONT_MARKER / CPF_FONT_MARKER_SMALL
uint16_t height; /// Newline distance (y axis)
uint16_t ascender; /// Maximal height of a glyph above the base line
uint8_t styles; /// Regular = 0x01, Bold = 0x02, Italic = 0x04, BoldItalic = 0x08, can be OR'd together
uint16_t intervalCount; /// Number of unicode intervals.
uint32_t glyphCount; /// Number of total glyphs across all styles
} CrossPointFontHeader;
/// Data stored for FONT AS A WHOLE
typedef struct {
CrossPointFontHeader header;
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
CrossPointFontGlyph* glyphs; /// Glyph array
uint8_t* bitmap; /// Glyph bitmaps, concatenated
} CrossPointFontData;
typedef struct {
CrossPointFontHeader header;
CrossPointFontUnicodeInterval* intervals; /// Valid unicode intervals for this font
CrossPointFontSmallGlyph* glyphs; /// Glyph array
uint8_t* bitmap; /// Glyph bitmaps, concatenated
} CrossPointFontDataSmall;

View File

@@ -0,0 +1,51 @@
#include "g5enc.inl"
#include "g5dec.inl"
//
// Group5 1-bit image compression library
// Written by Larry Bank (bitbank@pobox.com)
// Decoder C++ wrapper functions
//
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
int G5DECODER::init(int iWidth, int iHeight, uint8_t *pData, int iDataSize)
{
return g5_decode_init(&_g5dec, iWidth, iHeight, pData, iDataSize);
} /* init() */
int G5DECODER::decodeLine(uint8_t *pOut)
{
return g5_decode_line(&_g5dec, pOut);
} /* decodeLine() */
//
// Encoder C++ wrapper functions
//
int G5ENCODER::init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
{
return g5_encode_init(&_g5enc, iWidth, iHeight, pOut, iOutSize);
} /* init() */
int G5ENCODER::encodeLine(uint8_t *pPixels)
{
return g5_encode_encodeLine(&_g5enc, pPixels);
} /* encodeLine() */
int G5ENCODER::size()
{
return g5_encode_getOutSize(&_g5enc);
} /* size() */

View File

@@ -0,0 +1,171 @@
#ifndef __GROUP5__
#define __GROUP5__
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#ifdef __AVR__
#include <avr/pgmspace.h>
#endif
//
// Group5 1-bit image compression library
// Written by Larry Bank (bitbank@pobox.com)
//
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// The name "Group5" is derived from the CCITT Group4 standard
// This code is based on a lot of the good ideas from CCITT T.6
// for FAX image compression, but modified to work in a very
// constrained environment. The Huffman tables for horizontal
// mode have been replaced with a simple 2-bit flag followed by
// short or long counts of a fixed length. The short codes are
// always 3 bits (run lengths 0-7) and the long codes are the
// number of bits needed to encode the width of the image.
// For example, if a 320 pixel wide image is being compressed,
// the longest horizontal run needed is 320, which requires 9
// bits to encode. The 2 prefix bits have the following meaning:
// 00 = short, short (3+3 bits)
// 01 = short, long (3+N bits)
// 10 = long, short (N+3 bits)
// 11 = long, long (N+N bits)
// The rest of the code works identically to Group4 2D FAX
//
// Caution - this is the maximum number of color changes per line
// The default value is set low to work embedded systems with little RAM
// for font compression, this is plenty since each line of a character should have
// a maximum of 7 color changes
// You can define this in your compiler macros to override the default vlaue
//
#ifndef MAX_IMAGE_FLIPS
#ifdef __AVR__
#define MAX_IMAGE_FLIPS 32
#else
#define MAX_IMAGE_FLIPS 512
#endif // __AVR__
#endif
// Horizontal prefix bits
enum {
HORIZ_SHORT_SHORT=0,
HORIZ_SHORT_LONG,
HORIZ_LONG_SHORT,
HORIZ_LONG_LONG
};
// Return code for encoder and decoder
enum {
G5_SUCCESS = 0,
G5_INVALID_PARAMETER,
G5_DECODE_ERROR,
G5_UNSUPPORTED_FEATURE,
G5_ENCODE_COMPLETE,
G5_DECODE_COMPLETE,
G5_NOT_INITIALIZED,
G5_DATA_OVERFLOW,
G5_MAX_FLIPS_EXCEEDED
};
//
// Decoder state
//
typedef struct g5_dec_image_tag
{
int iWidth, iHeight; // image size
int iError;
int y; // last y value drawn
int iVLCSize;
int iHLen; // length of 'long' horizontal codes for this image
int iPitch; // width in bytes of output buffer
uint32_t u32Accum; // fractional scaling accumulator
uint32_t ulBitOff, ulBits; // vlc decode variables
uint8_t *pSrc, *pBuf; // starting & current buffer pointer
int16_t *pCur, *pRef; // current state of current vs reference flips
int16_t CurFlips[MAX_IMAGE_FLIPS];
int16_t RefFlips[MAX_IMAGE_FLIPS];
} G5DECIMAGE;
// Due to unaligned memory causing an exception, we have to do these macros the slow way
#ifdef __AVR__
// assume PROGMEM as the source of data
inline uint32_t TIFFMOTOLONG(uint8_t *p)
{
uint32_t u32 = pgm_read_dword(p);
return __builtin_bswap32(u32);
}
#else
#define TIFFMOTOLONG(p) (((uint32_t)(*p)<<24UL) + ((uint32_t)(*(p+1))<<16UL) + ((uint32_t)(*(p+2))<<8UL) + (uint32_t)(*(p+3)))
#endif // __AVR__
#define TOP_BIT 0x80000000
#define MAX_VALUE 0xffffffff
// Must be a 32-bit target processor
#define REGISTER_WIDTH 32
#define BIGUINT uint32_t
//
// G5 Encoder
//
typedef struct g5_buffered_bits
{
unsigned char *pBuf; // buffer pointer
uint32_t ulBits; // buffered bits
uint32_t ulBitOff; // current bit offset
uint32_t ulDataSize; // available data
} G5_BUFFERED_BITS;
//
// Encoder state
//
typedef struct g5_enc_image_tag
{
int iWidth, iHeight; // image size
int iError;
int y; // last y encoded
int iOutSize;
int iDataSize; // generated output size
uint8_t *pOutBuf;
int16_t *pCur, *pRef; // pointers to swap current and reference lines
G5_BUFFERED_BITS bb;
int16_t CurFlips[MAX_IMAGE_FLIPS];
int16_t RefFlips[MAX_IMAGE_FLIPS];
} G5ENCIMAGE;
#ifdef __cplusplus
//
// The G5 classes wrap portable C code which does the actual work
//
class G5ENCODER
{
public:
int init(int iWidth, int iHeight, uint8_t *pOut, int iOutSize);
int encodeLine(uint8_t *pPixels);
int size();
private:
G5ENCIMAGE _g5enc;
};
class G5DECODER
{
public:
int init(int iWidth, int iHeight, uint8_t *pData, int iDataSize);
int decodeLine(uint8_t *pOut);
private:
G5DECIMAGE _g5dec;
};
#endif // __cplusplus
#endif // __GROUP5__

View File

@@ -0,0 +1,346 @@
//
// Group5
// A 1-bpp image decoder
//
// Written by Larry Bank (bitbank@pobox.com)
//
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "Group5.h"
/*
The code tree that follows has: bit_length, decode routine
These codes are for Group 4 (MMR) decoding
01 = vertneg1, 11h = vert1, 20h = horiz, 30h = pass, 12h = vert2
02 = vertneg2, 13h = vert3, 03 = vertneg3, 90h = trash
*/
static const uint8_t code_table[128] =
{0x90, 0, 0x40, 0, /* trash, uncompr mode - codes 0 and 1 */
3, 7, /* V(-3) pos = 2 */
0x13, 7, /* V(3) pos = 3 */
2, 6, 2, 6, /* V(-2) pos = 4,5 */
0x12, 6, 0x12, 6, /* V(2) pos = 6,7 */
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4, /* pass pos = 8->F */
0x30, 4, 0x30, 4, 0x30, 4, 0x30, 4,
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3, /* horiz pos = 10->1F */
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
0x20, 3, 0x20, 3, 0x20, 3, 0x20, 3,
/* V(-1) pos = 20->2F */
1, 3, 1, 3, 1, 3, 1, 3,
1, 3, 1, 3, 1, 3, 1, 3,
1, 3, 1, 3, 1, 3, 1, 3,
1, 3, 1, 3, 1, 3, 1, 3,
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3, /* V(1) pos = 30->3F */
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3,
0x11, 3, 0x11, 3, 0x11, 3, 0x11, 3};
static int g5_decode_init(G5DECIMAGE *pImage, int iWidth, int iHeight, uint8_t *pData, int iDataSize)
{
if (pImage == NULL || iWidth < 1 || iHeight < 1 || pData == NULL || iDataSize < 1)
return G5_INVALID_PARAMETER;
pImage->iVLCSize = iDataSize;
pImage->pSrc = pData;
pImage->ulBitOff = 0;
pImage->y = 0;
pImage->ulBits = TIFFMOTOLONG(pData); // preload the first 32 bits of data
pImage->iWidth = iWidth;
pImage->iHeight = iHeight;
return G5_SUCCESS;
} /* g5_decode_init() */
static void G5DrawLine(G5DECIMAGE *pPage, int16_t *pCurFlips, uint8_t *pOut)
{
int x, len, run;
uint8_t lBit, rBit, *p;
int iStart = 0, xright = pPage->iWidth;
uint8_t *pDest;
iStart = 0;
pDest = pOut;
len = (xright+7)>>3; // number of bytes to generate
for (x=0; x<len; x++) {
pOut[x] = 0xff; // start with white and only draw the black runs
}
x = 0;
while (x < xright) { // while the scaled x is within the window bounds
x = *pCurFlips++; // black starting point
run = *pCurFlips++ - x; // get the black run
x -= iStart;
if (x >= xright || run == 0)
break;
if ((x + run) > 0) { /* If the run is visible, draw it */
if (x < 0) {
run += x; /* draw only visible part of run */
x = 0;
}
if ((x + run) > xright) { /* Don't let it go off right edge */
run = xright - x;
}
/* Draw this run */
lBit = 0xff << (8 - (x & 7));
rBit = 0xff >> ((x + run) & 7);
len = ((x+run)>>3) - (x >> 3);
p = &pDest[x >> 3];
if (len == 0) {
lBit |= rBit;
*p &= lBit;
} else {
*p++ &= lBit;
while (len > 1) {
*p++ = 0;
len--;
}
*p = rBit;
}
} // visible run
} /* while drawing line */
} /* G5DrawLine() */
//
// Initialize internal structures to decode the image
//
static void Decode_Begin(G5DECIMAGE *pPage)
{
int i, xsize;
int16_t *CurFlips, *RefFlips;
xsize = pPage->iWidth;
RefFlips = pPage->RefFlips;
CurFlips = pPage->CurFlips;
/* Seed the current and reference line with XSIZE for V(0) codes */
for (i=0; i<MAX_IMAGE_FLIPS-2; i++) {
RefFlips[i] = xsize;
CurFlips[i] = xsize;
}
/* Prefill both current and reference lines with 7fff to prevent it from
walking off the end if the data gets bunged and the current X is > XSIZE
3-16-94 */
CurFlips[i] = RefFlips[i] = 0x7fff;
CurFlips[i+1] = RefFlips[i+1] = 0x7fff;
pPage->pCur = CurFlips;
pPage->pRef = RefFlips;
pPage->pBuf = pPage->pSrc;
pPage->ulBits = TIFFMOTOLONG(pPage->pSrc); // load 32 bits to start
pPage->ulBitOff = 0;
// Calculate the number of bits needed for a long horizontal code
#ifdef __AVR__
pPage->iHLen = 16 - __builtin_clz(pPage->iWidth);
#else
pPage->iHLen = 32 - __builtin_clz(pPage->iWidth);
#endif
} /* Decode_Begin() */
//
// Decode a single line of G5 data (private function)
//
static int DecodeLine(G5DECIMAGE *pPage)
{
signed int a0, a0_p, b1;
int16_t *pCur, *pRef, *RefFlips, *CurFlips;
int xsize, tot_run=0, tot_run1 = 0;
int32_t sCode;
uint32_t lBits;
uint32_t ulBits, ulBitOff;
uint8_t *pBuf/*, *pBufEnd*/;
uint32_t u32HMask, u32HLen; // horizontal code mask and length
pCur = CurFlips = pPage->pCur;
pRef = RefFlips = pPage->pRef;
ulBits = pPage->ulBits;
ulBitOff = pPage->ulBitOff;
pBuf = pPage->pBuf;
// pBufEnd = &pPage->pSrc[pPage->iVLCSize];
u32HLen = pPage->iHLen;
u32HMask = (1 << u32HLen) - 1;
a0 = -1;
xsize = pPage->iWidth;
while (a0 < xsize) { /* Decode this line */
if (ulBitOff > (REGISTER_WIDTH-8)) { // need at least 7 unused bits
pBuf += (ulBitOff >> 3);
ulBitOff &= 7;
ulBits = TIFFMOTOLONG(pBuf);
}
if ((int32_t)(ulBits << ulBitOff) < 0) { /* V(0) code is the most frequent case (1 bit) */
a0 = *pRef++;
ulBitOff++; // length = 1 bit
*pCur++ = a0;
} else { /* Slower method for the less frequence codes */
lBits = (ulBits >> ((REGISTER_WIDTH - 8) - ulBitOff)) & 0xfe; /* Only the first 7 bits are useful */
sCode = code_table[lBits]; /* Get the code type as an 8-bit value */
ulBitOff += code_table[lBits+1]; /* Get the code length */
switch (sCode) {
case 1: /* V(-1) */
case 2: /* V(-2) */
case 3: /* V(-3) */
a0 = *pRef - sCode; /* A0 = B1 - x */
*pCur++ = a0;
if (pRef == RefFlips) {
pRef += 2;
}
pRef--;
while (a0 >= *pRef) {
pRef += 2;
}
break;
case 0x11: /* V(1) */
case 0x12: /* V(2) */
case 0x13: /* V(3) */
a0 = *pRef++; /* A0 = B1 */
b1 = a0;
a0 += sCode & 7; /* A0 = B1 + x */
if (b1 != xsize && a0 < xsize) {
while (a0 >= *pRef) {
pRef += 2;
}
}
if (a0 > xsize) {
a0 = xsize;
}
*pCur++ = a0;
break;
case 0x20: /* Horizontal codes */
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
pBuf += (ulBitOff >> 3);
ulBitOff &= 7;
ulBits = TIFFMOTOLONG(pBuf);
}
a0_p = a0;
if (a0 < 0) {
a0_p = 0;
}
lBits = (ulBits >> ((REGISTER_WIDTH - 2) - ulBitOff)) & 0x3; // get 2-bit prefix for code type
// There are 4 possible horizontal cases: short/short, short/long, long/short, long/long
// These are encoded in a 2-bit prefix code, followed by 3 bits for short or N bits for long code
// N is the log base 2 of the image width (e.g. 320 pixels requires 9 bits)
ulBitOff += 2;
switch (lBits) {
case HORIZ_SHORT_SHORT:
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
ulBitOff += 3;
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
ulBitOff += 3;
break;
case HORIZ_SHORT_LONG:
tot_run = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
ulBitOff += 3;
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
ulBitOff += u32HLen;
break;
case HORIZ_LONG_SHORT:
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
ulBitOff += u32HLen;
tot_run1 = (ulBits >> ((REGISTER_WIDTH - 3) - ulBitOff)) & 0x7; // get 3-bit short length
ulBitOff += 3;
break;
case HORIZ_LONG_LONG:
tot_run = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
ulBitOff += u32HLen;
if (ulBitOff > (REGISTER_WIDTH-16)) { // need at least 16 unused bits
pBuf += (ulBitOff >> 3);
ulBitOff &= 7;
ulBits = TIFFMOTOLONG(pBuf);
}
tot_run1 = (ulBits >> ((REGISTER_WIDTH - u32HLen) - ulBitOff)) & u32HMask; // get long length
ulBitOff += u32HLen;
break;
} // switch on lBits
a0 = a0_p + tot_run;
*pCur++ = a0;
a0 += tot_run1;
if (a0 < xsize) {
while (a0 >= *pRef) {
pRef += 2;
}
}
*pCur++ = a0;
break;
case 0x30: /* Pass code */
pRef++; /* A0 = B2, iRef+=2 */
a0 = *pRef++;
break;
default: /* ERROR */
pPage->iError = G5_DECODE_ERROR;
goto pilreadg5z;
} /* switch */
} /* Slow climb */
}
/*--- Convert flips data into run lengths ---*/
*pCur++ = xsize; /* Terminate the line properly */
*pCur++ = xsize;
pilreadg5z:
// Save the current VLC decoder state
pPage->ulBits = ulBits;
pPage->ulBitOff = ulBitOff;
pPage->pBuf = pBuf;
return pPage->iError;
} /* DecodeLine() */
//
// Decompress the VLC data
//
static int g5_decode_line(G5DECIMAGE *pPage, uint8_t *pOut)
{
int rc;
uint8_t *pBufEnd;
int16_t *t1;
if (pPage == NULL || pOut == NULL)
return G5_INVALID_PARAMETER;
if (pPage->y >= pPage->iHeight)
return G5_DECODE_COMPLETE;
pPage->iError = G5_SUCCESS;
if (pPage->y == 0) { // first time through
Decode_Begin(pPage);
}
pBufEnd = &pPage->pSrc[pPage->iVLCSize];
if (pPage->pBuf >= pBufEnd) { // read past the end, error
pPage->iError = G5_DECODE_ERROR;
return G5_DECODE_ERROR;
}
rc = DecodeLine(pPage);
if (rc == G5_SUCCESS) {
// Draw the current line
G5DrawLine(pPage, pPage->pCur, pOut);
/*--- Swap current and reference lines ---*/
t1 = pPage->pRef;
pPage->pRef = pPage->pCur;
pPage->pCur = t1;
pPage->y++;
if (pPage->y >= pPage->iHeight) {
pPage->iError = G5_DECODE_COMPLETE;
}
} else {
pPage->iError = rc;
}
return pPage->iError;
} /* Decode() */

View File

@@ -0,0 +1,315 @@
//
// G5 Encoder
// A 1-bpp image encoding library
//
// Written by Larry Bank (bitbank@pobox.com)
//
// SPDX-FileCopyrightText: 2024 BitBank Software, Inc.
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
#include "Group5.h"
/* Number of consecutive 1 bits in a byte from MSB to LSB */
static uint8_t bitcount[256] =
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0-15 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 16-31 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 32-47 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 48-63 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 64-79 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 80-95 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 96-111 */
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 112-127 */
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 128-143 */
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 144-159 */
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 160-175 */
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, /* 176-191 */
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 192-207 */
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, /* 208-223 */
3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, /* 224-239 */
4,4,4,4,4,4,4,4,5,5,5,5,6,6,7,8}; /* 240-255 */
/* Table of vertical codes for G5 encoding */
/* code followed by length, starting with v(-3) */
static const uint8_t vtable[14] =
{3,7, /* V(-3) = 0000011 */
3,6, /* V(-2) = 000011 */
3,3, /* V(-1) = 011 */
1,1, /* V(0) = 1 */
2,3, /* V(1) = 010 */
2,6, /* V(2) = 000010 */
2,7}; /* V(3) = 0000010 */
static void G5ENCInsertCode(G5_BUFFERED_BITS *bb, BIGUINT ulCode, int iLen)
{
if ((bb->ulBitOff + iLen) > REGISTER_WIDTH) { // need to write data
bb->ulBits |= (ulCode >> (bb->ulBitOff + iLen - REGISTER_WIDTH)); // partial bits on first word
*(BIGUINT *)bb->pBuf = __builtin_bswap32(bb->ulBits);
bb->pBuf += sizeof(BIGUINT);
bb->ulBits = ulCode << ((REGISTER_WIDTH*2) - (bb->ulBitOff + iLen));
bb->ulBitOff += iLen - REGISTER_WIDTH;
} else {
bb->ulBits |= (ulCode << (REGISTER_WIDTH - bb->ulBitOff - iLen));
bb->ulBitOff += iLen;
}
} /* G5ENCInsertCode() */
//
// Flush any buffered bits to the output
//
static void G5ENCFlushBits(G5_BUFFERED_BITS *bb)
{
while (bb->ulBitOff >= 8)
{
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
bb->ulBits <<= 8;
bb->ulBitOff -= 8;
}
if (bb->ulBitOff) { // partial byte?
*bb->pBuf++ = (unsigned char) (bb->ulBits >> (REGISTER_WIDTH - 8));
}
bb->ulBitOff = 0;
bb->ulBits = 0;
} /* G5ENCFlushBits() */
//
// Initialize the compressor
// This must be called before adding data to the output
//
static int g5_encode_init(G5ENCIMAGE *pImage, int iWidth, int iHeight, uint8_t *pOut, int iOutSize)
{
int iError = G5_SUCCESS;
if (pImage == NULL || iHeight <= 0)
return G5_INVALID_PARAMETER;
pImage->iWidth = iWidth; // image size
pImage->iHeight = iHeight;
pImage->pCur = pImage->CurFlips;
pImage->pRef = pImage->RefFlips;
pImage->pOutBuf = pOut; // optional output buffer
pImage->iOutSize = iOutSize; // output buffer pre-allocated size
pImage->iDataSize = 0; // no data yet
pImage->y = 0;
for (int i=0; i<MAX_IMAGE_FLIPS; i++) {
pImage->RefFlips[i] = iWidth;
pImage->CurFlips[i] = iWidth;
}
pImage->bb.pBuf = pImage->pOutBuf;
pImage->bb.ulBits = 0;
pImage->bb.ulBitOff = 0;
pImage->iError = iError;
return iError;
} /* g5_encode_init() */
//
// Internal function to convert uncompressed 1-bit per pixel data
// into the run-end data needed to feed the G5 encoder
//
static int G5ENCEncodeLine(unsigned char *buf, int xsize, int16_t *pDest)
{
int iCount, xborder;
uint8_t i, c;
int8_t cBits;
int iLen;
int16_t x;
int16_t *pLimit = pDest + (MAX_IMAGE_FLIPS-4);
xborder = xsize;
iCount = (xsize + 7) >> 3; /* Number of bytes per line */
cBits = 8;
iLen = 0; /* Current run length */
x = 0;
c = *buf++; /* Get the first byte to start */
iCount--;
while (iCount >=0) {
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
i = bitcount[c]; /* Get the number of consecutive bits */
iLen += i; /* Add this length to total run length */
c <<= i;
cBits -= i; /* Minus the number in a byte */
if (cBits <= 0)
{
iLen += cBits; /* Adjust length */
cBits = 8;
c = *buf++; /* Get another data byte */
iCount--;
continue; /* Keep doing white until color change */
}
c = ~c; /* flip color to count black pixels */
/* Store the white run length */
xborder -= iLen;
if (xborder < 0)
{
iLen += xborder; /* Make sure run length is not past end */
break;
}
x += iLen;
*pDest++ = x;
iLen = 0;
doblack:
i = bitcount[c]; /* Get consecutive bits */
iLen += i; /* Add to total run length */
c <<= i;
cBits -= i;
if (cBits <= 0)
{
iLen += cBits; /* Adjust length */
cBits = 8;
c = *buf++; /* Get another data byte */
c = ~c; /* Flip color to find black */
iCount--;
if (iCount < 0)
break;
goto doblack;
}
/* Store the black run length */
c = ~c; /* Flip color again to find white pixels */
xborder -= iLen;
if (xborder < 0)
{
iLen += xborder; /* Make sure run length is not past end */
break;
}
x += iLen;
*pDest++ = x;
iLen = 0;
} /* while */
if (pDest >= pLimit) return G5_MAX_FLIPS_EXCEEDED;
*pDest++ = xsize;
*pDest++ = xsize; // Store a few more XSIZE to end the line
*pDest++ = xsize; // so that the compressor doesn't go past
*pDest++ = xsize; // the end of the line
return G5_SUCCESS;
} /* G5ENCEncodeLine() */
//
// Compress a line of pixels and add it to the output
// the input format is expected to be MSB (most significant bit) first
// for example, pixel 0 is in byte 0 at bit 7 (0x80)
// Returns G5ENC_SUCCESS for each line if all is well and G5ENC_IMAGE_COMPLETE
// for the last line
//
static int g5_encode_encodeLine(G5ENCIMAGE *pImage, uint8_t *pPixels)
{
int16_t a0, a0_c, b2, a1;
int dx, run1, run2;
int xsize, iErr, iHighWater;
int iCur, iRef, iLen;
int iHLen; // number of bits for long horizontal codes
int16_t *CurFlips, *RefFlips;
G5_BUFFERED_BITS bb;
if (pImage == NULL || pPixels == NULL)
return G5_INVALID_PARAMETER;
iHighWater = pImage->iOutSize - 32;
iHLen = 32 - __builtin_clz(pImage->iWidth);
memcpy(&bb, &pImage->bb, sizeof(G5_BUFFERED_BITS)); // keep local copy
CurFlips = pImage->pCur;
RefFlips = pImage->pRef;
xsize = pImage->iWidth; /* For performance reasons */
// Convert the incoming line of pixels into run-end data
iErr = G5ENCEncodeLine(pPixels, pImage->iWidth, CurFlips);
if (iErr != G5_SUCCESS) return iErr; // exceeded the maximum number of color changes
/* Encode this line as G5 */
a0 = a0_c = 0;
iCur = iRef = 0;
while (a0 < xsize) {
b2 = RefFlips[iRef+1];
a1 = CurFlips[iCur];
if (b2 < a1) { /* Is b2 to the left of a1? */
/* yes, do pass mode */
a0 = b2;
iRef += 2;
G5ENCInsertCode(&bb, 1, 4); /* Pass code = 0001 */
} else { /* Try vertical and horizontal mode */
dx = RefFlips[iRef] - a1; /* b1 - a1 */
if (dx > 3 || dx < -3) { /* Horizontal mode */
G5ENCInsertCode(&bb, 1, 3); /* Horizontal code = 001 */
run1 = CurFlips[iCur] - a0;
run2 = CurFlips[iCur+1] - CurFlips[iCur];
if (run1 < 8) {
if (run2 < 8) { // short, short
G5ENCInsertCode(&bb, HORIZ_SHORT_SHORT, 2); /* short, short = 00 */
G5ENCInsertCode(&bb, run1, 3);
G5ENCInsertCode(&bb, run2, 3);
} else { // short, long
G5ENCInsertCode(&bb, HORIZ_SHORT_LONG, 2); /* short, long = 01 */
G5ENCInsertCode(&bb, run1, 3);
G5ENCInsertCode(&bb, run2, iHLen);
}
} else { // first run is long
if (run2 < 8) { // long, short
G5ENCInsertCode(&bb, HORIZ_LONG_SHORT, 2); /* long, short = 10 */
G5ENCInsertCode(&bb, run1, iHLen);
G5ENCInsertCode(&bb, run2, 3);
} else { // long, long
G5ENCInsertCode(&bb, HORIZ_LONG_LONG, 2); /* long, long = 11 */
G5ENCInsertCode(&bb, run1, iHLen);
G5ENCInsertCode(&bb, run2, iHLen);
}
}
a0 = CurFlips[iCur+1]; /* a0 = a2 */
if (a0 != xsize) {
iCur += 2; /* Skip two color flips */
while (RefFlips[iRef] != xsize && RefFlips[iRef] <= a0) {
iRef += 2;
}
}
} else { /* Vertical mode */
dx = (dx + 3) * 2; /* Convert to index table */
G5ENCInsertCode(&bb, vtable[dx], vtable[dx+1]);
a0 = a1;
a0_c = 1-a0_c;
if (a0 != xsize) {
if (iRef != 0) {
iRef -= 2;
}
iRef++; /* Skip a color change in cur and ref */
iCur++;
while (RefFlips[iRef] <= a0 && RefFlips[iRef] != xsize) {
iRef += 2;
}
}
} /* vertical mode */
} /* horiz/vert mode */
} /* while x < xsize */
iLen = (int)(bb.pBuf-pImage->pOutBuf);
if (iLen >= iHighWater) { // not enough space
pImage->iError = iErr = G5_DATA_OVERFLOW; // we don't have a better error
return iErr;
}
if (pImage->y == pImage->iHeight-1) { // last line of image
G5ENCFlushBits(&bb); // output the final buffered bits
// wrap up final output
pImage->iDataSize = 1 + (int)(bb.pBuf-pImage->pOutBuf);
iErr = G5_ENCODE_COMPLETE;
}
pImage->pCur = RefFlips; // swap current and reference lines
pImage->pRef = CurFlips;
pImage->y++;
memcpy(&pImage->bb, &bb, sizeof(bb));
return iErr;
} /* g5_encode_encodeLine() */
//
// Returns the number of bytes of G5 created by the encoder
//
static int g5_encode_getOutSize(G5ENCIMAGE *pImage)
{
int iSize = 0;
if (pImage != NULL)
iSize = pImage->iDataSize;
return iSize;
} /* g5_encode_getOutSize() */

View File

@@ -0,0 +1,17 @@
#pragma once
#include <builtinFonts/bookerly_12.h>
#include <builtinFonts/bookerly_14.h>
#include <builtinFonts/bookerly_16.h>
#include <builtinFonts/bookerly_18.h>
#include <builtinFonts/notosans_8.h>
#include <builtinFonts/notosans_12.h>
#include <builtinFonts/notosans_14.h>
#include <builtinFonts/notosans_16.h>
#include <builtinFonts/notosans_18.h>
#include <builtinFonts/opendyslexic_8.h>
#include <builtinFonts/opendyslexic_10.h>
#include <builtinFonts/opendyslexic_12.h>
#include <builtinFonts/opendyslexic_14.h>
#include <builtinFonts/ubuntu_10.h>
#include <builtinFonts/ubuntu_12.h>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
echo "// The contents of this file are generated by ./lib/CrossPointFont/builtinFonts/build-font-ids.sh"
echo "#pragma once"
echo ""
echo "#define BOOKERLY_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define BOOKERLY_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define BOOKERLY_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define BOOKERLY_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./bookerly_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define NOTOSANS_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define NOTOSANS_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define NOTOSANS_16_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_16.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define NOTOSANS_18_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_18.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define OPENDYSLEXIC_8_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define OPENDYSLEXIC_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define OPENDYSLEXIC_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define OPENDYSLEXIC_14_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./opendyslexic_14.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define UI_10_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_10.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define UI_12_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./ubuntu_12.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"
echo "#define SMALL_FONT_ID ($(ruby -rdigest -e 'puts Digest::SHA256.hexdigest(File.read("./notosans_8.h")).to_i(16) % (2 ** 32) - (2 ** 31)'))"

View File

@@ -0,0 +1,42 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
BOOKERLY_FONT_SIZES=(12 14 16 18)
NOTOSANS_FONT_SIZES=(12 14 16 18)
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
for size in ${BOOKERLY_FONT_SIZES[@]}; do
font_name="bookerly_${size}"
font_path_prefix="./source/Bookerly/Bookerly-"
output_path="./${font_name}.h"
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
done
for size in ${NOTOSANS_FONT_SIZES[@]}; do
font_name="notosans_${size}"
font_path_prefix="./source/NotoSans/NotoSans-"
output_path="./${font_name}.h"
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -i "${font_path_prefix}Italic.ttf" -bi "${font_path_prefix}BoldItalic.ttf" -o $output_path -p $size
done
for size in ${OPENDYSLEXIC_FONT_SIZES[@]}; do
font_name="opendyslexic_${size}"
font_path_prefix="./source/OpenDyslexic/OpenDyslexic-"
output_path="./${font_name}.h"
../fontconvert/fontconvert "${font_path_prefix}Regular.otf" -b "${font_path_prefix}Bold.otf" -i "${font_path_prefix}Italic.otf" -bi "${font_path_prefix}BoldItalic.otf" -o $output_path -p $size
done
UI_FONT_SIZES=(10 12)
UI_FONT_STYLES=("Regular" "Bold")
for size in ${UI_FONT_SIZES[@]}; do
font_name="ubuntu_${size}"
font_path_prefix="./source/Ubuntu/Ubuntu-"
output_path="./${font_name}.h"
../fontconvert/fontconvert "${font_path_prefix}Regular.ttf" -b "${font_path_prefix}Bold.ttf" -o $output_path -p $size
done
../fontconvert/fontconvert ./source/NotoSans/NotoSans-Regular.ttf -o ./notosans_8.h -p 8

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,94 @@
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
with Reserved Font Name OpenDyslexic.
Copyright (c) 12/2012 - 2019
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,96 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

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 @@
fontconvert

View File

@@ -0,0 +1,12 @@
all: fontconvert
CC = gcc
CFLAGS = -Wall -I/usr/local/include/freetype2 -I/usr/include/freetype2 -I/usr/include -I/opt/homebrew/include/freetype2
LIBS = -L/opt/homebrew/lib -lfreetype
fontconvert: main.c ../Group5/Group5.h
$(CC) $(CFLAGS) main.c $(LIBS) -o fontconvert
strip fontconvert
clean:
rm -f fontconvert

View File

@@ -0,0 +1,553 @@
//
// TrueType to CrossPoint font converter
// Copyright (c) 2024 BitBank Software, inc.
// Written by Larry Bank, adapted by Dave Allie for CrossPoint
// August 31, 2024
// The CrossPoint font format is a losslessly compressed bitmap font of a single point size with multiple variants
// This was built entirely on the back of Larry Bank's bb_font format.
// The data is compressed with a compression scheme based on CCITT T.6
// The font structure includes overall size, per-character glyph info and then the
// compressed image data at the end.
// The font file format is designed to allow both dynamic loading of font data from
// external memory/disk or compiling the data as const into a progarm.
//
// Example usage:
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.cpf>
// ./fontconvert <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p <pt size> -o <out.h>
//
// This code requires the freetype library
// found here: www.freetype.org
//
#ifndef ARDUINO
#include <ctype.h>
#include <ft2build.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../CrossPointFontFormat.h"
#include FT_GLYPH_H
#include FT_MODULE_H
#include FT_TRUETYPE_DRIVER_H
#include "../Group5/g5enc.inl" // Group5 image compression library
G5ENCIMAGE g5enc; // Group5 encoder state
#define DPI 150 // Approximate resolution of common displays
#define OUTBUF_SIZE 1048576 // 1MB
#define MAX_INTERVALS 65536
#define FONT_SCALE_FACTOR 2
// TODO: Re-enable small font
// Disabled small font generation to get this working, but want to re-enable
#define SMALL_FONT_ENABLED 0
uint32_t raw_intervals[][2] = {
/* Basic Latin */
// ASCII letters, digits, punctuation, control characters
{0x0000, 0x007F},
/* Latin-1 Supplement */
// Accented characters for Western European languages
{0x0080, 0x00FF},
/* Latin Extended-A */
// Eastern European and Baltic languages
{0x0100, 0x017F},
/* General Punctuation (core subset) */
// Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
{0x2000, 0x206F},
/* Basic Symbols From "Latin-1 + Misc" */
// dashes, quotes, prime marks
{0x2010, 0x203A},
// misc punctuation
{0x2040, 0x205F},
// common currency symbols
{0x20A0, 0x20CF},
/* Combining Diacritical Marks (minimal subset) */
// Needed for proper rendering of many extended Latin languages
{0x0300, 0x036F},
/* Greek & Coptic */
// Used in science, maths, philosophy, some academic texts
// {0x0370, 0x03FF},
/* Cyrillic */
// Russian, Ukrainian, Bulgarian, etc.
{0x0400, 0x04FF},
/* Math Symbols (common subset) */
// Superscripts and Subscripts
{0x2070, 0x209F},
// General math operators
{0x2200, 0x22FF},
// Arrows
{0x2190, 0x21FF},
/* CJK */
// Core Unified Ideographs
// {0x4E00, 0x9FFF},
// Extension A
// {0x3400, 0x4DBF},
// Extension B
// {0x20000, 0x2A6DF},
// Extension CF
// {0x2A700, 0x2EBEF},
// Extension G
// {0x30000, 0x3134F},
// Hiragana
// {0x3040, 0x309F},
// Katakana
// {0x30A0, 0x30FF},
// Katakana Phonetic Extensions
// {0x31F0, 0x31FF},
// Halfwidth Katakana
// {0xFF60, 0xFF9F},
// Hangul Syllables
// {0xAC00, 0xD7AF},
// Hangul Jamo
// {0x1100, 0x11FF},
// Hangul Compatibility Jamo
// {0x3130, 0x318F},
// Hangul Jamo Extended-A
// {0xA960, 0xA97F},
// Hangul Jamo Extended-B
// {0xD7B0, 0xD7FF},
// CJK Radicals Supplement
// {0x2E80, 0x2EFF},
// Kangxi Radicals
// {0x2F00, 0x2FDF},
// CJK Symbols and Punctuation
// {0x3000, 0x303F},
// CJK Compatibility Forms
// {0xFE30, 0xFE4F},
// CJK Compatibility Ideographs
// {0xF900, 0xFAFF},
/* Specials */
// Replacement Character
{0xFFFD, 0xFFFD},
};
//
// Comparison function for qsort
//
int compareIntervals(const void* a, const void* b) {
const uint32_t* ia = (uint32_t*)a;
const uint32_t* ib = (uint32_t*)b;
if (ia[0] < ib[0]) return -1;
if (ia[0] > ib[0]) return 1;
return 0;
}
//
// Sort and merge adjacent intervals
// Returns the number of intervals after merging
//
int sortAndMergeIntervals(uint32_t intervals[][2], const int count) {
int merged_count = 0;
// Sort intervals by start value
qsort(intervals, count, sizeof(uint32_t) * 2, compareIntervals);
// Merge overlapping/adjacent intervals
for (int i = 0; i < count; i++) {
if (merged_count > 0 && intervals[i][0] <= intervals[merged_count - 1][1] + 1) {
// Merge with previous interval
if (intervals[i][1] > intervals[merged_count - 1][1]) {
intervals[merged_count - 1][1] = intervals[i][1];
}
} else {
// Add as new interval
if (merged_count != i) {
intervals[merged_count][0] = intervals[i][0];
intervals[merged_count][1] = intervals[i][1];
}
merged_count++;
}
}
return merged_count;
}
//
// Create the comments and const array boilerplate for the hex data bytes
//
void StartHexFile(FILE* f, int iLen, const char* fname, int size) {
int i, j;
char szTemp[256];
fprintf(f, "#pragma once\n\n");
fprintf(f, "//\n// Created with fontconvert, written by Larry Bank, updated for CrossPoint by Dave Allie\n");
fprintf(f, "// Point size: %d (scaled %dx)\n", size, FONT_SCALE_FACTOR);
fprintf(f, "// compressed font data size = %d bytes\n//\n", iLen);
strcpy(szTemp, fname);
i = strlen(szTemp);
if (szTemp[i - 2] == '.') szTemp[i - 2] = 0; // get the leaf name for the data
j = i;
// go backwards to get rid trim off just the leaf name
while (j > 0 && szTemp[j] != '/') {
j--;
}
if (szTemp[j] == '/') j++;
fprintf(f, "static const uint8_t %s[] = {\n", &szTemp[j]);
} /* StartHexFile() */
//
// Add N bytes of hex data to the output
// The data will be arranged in rows of 16 bytes each
//
void AddHexBytes(FILE* f, void* pData, int iLen, int bLast) {
static int iCount = 0; // number of bytes processed so far
int i;
uint8_t* s = (uint8_t*)pData;
for (i = 0; i < iLen; i++) { // process the given data
fprintf(f, "0x%02x", *s++);
iCount++;
if (i < iLen - 1 || !bLast) fprintf(f, ",");
if ((iCount & 15) == 0) fprintf(f, "\n"); // next row of 16
}
if (bLast) {
fprintf(f, "};\n");
}
} /* AddHexBytes() */
int loadCodePoint(FT_Face face, uint32_t code_point, CrossPointFontGlyph* pGlyphs, uint8_t* pBitmap,
uint32_t* glyph_index, uint32_t* iOffset) {
uint8_t* s;
int iPitch, err;
FT_Glyph glyph;
// MONO renderer provides clean image with perfect crop
// (no wasted pixels) via bitmap struct.
if ((err = FT_Load_Char(face, code_point, FT_LOAD_TARGET_MONO))) {
printf("Error %d loading char U+%04X\n", err, code_point);
(*glyph_index)++;
return 0;
}
if ((err = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_MONO))) {
printf("Error %d rendering char U+%04X\n", err, code_point);
(*glyph_index)++;
return 0;
}
if ((err = FT_Get_Glyph(face->glyph, &glyph))) {
printf("Error %d getting glyph U+%04X\n", err, code_point);
(*glyph_index)++;
return 0;
}
FT_Bitmap* bitmap = &face->glyph->bitmap;
FT_BitmapGlyphRec* g = (FT_BitmapGlyphRec*)glyph;
// TODO: Restore small font
if (0 /* bSmallFont */) {
#if SMALL_FONT_ENABLED == 1
printf("Stubbed\n");
return 1;
#else
printf("Small font has been disabled\n");
return 1;
#endif
} else {
pGlyphs[*glyph_index].bitmapOffset = *iOffset;
pGlyphs[*glyph_index].width = bitmap->width;
pGlyphs[*glyph_index].height = bitmap->rows;
pGlyphs[*glyph_index].xAdvance = (face->glyph->advance.x >> 6);
pGlyphs[*glyph_index].xOffset = g->left;
pGlyphs[*glyph_index].yOffset = g->top;
}
s = bitmap->buffer;
iPitch = bitmap->pitch;
g5_encode_init(&g5enc, bitmap->width, bitmap->rows, &pBitmap[*iOffset], OUTBUF_SIZE - *iOffset);
for (int y = 0; y < bitmap->rows; y++) {
g5_encode_encodeLine(&g5enc, &s[y * iPitch]);
} // for y
int iLen = g5_encode_getOutSize(&g5enc);
*iOffset += iLen;
FT_Done_Glyph(glyph);
(*glyph_index)++;
return 0;
}
int main(int argc, char* argv[]) {
int i, err, size = 0;
uint32_t iLen, iOffset = 0;
FILE* fOut;
// TrueType library structures
FT_Library library;
char* regularFaceFile;
char* boldFaceFile = NULL;
char* italicFaceFile = NULL;
char* boldItalicFaceFile = NULL;
const char* outputFile = NULL;
FT_Face faceRegular;
FT_Face faceBold;
FT_Face faceItalic;
FT_Face faceBoldItalic;
int bSmallFont = 0; // indicates if we're creating a normal or small font file
CrossPointFontUnicodeInterval* pIntervals;
CrossPointFontGlyph* pGlyphs;
#if SMALL_FONT_ENABLED == 1
CrossPointFontSmallGlyph* pSmallGlyphs;
#endif
uint8_t* pBitmap;
CrossPointFontHeader epdFontHeader;
int bHFile; // flag indicating if the output will be a .H file of hex data
// Process intervals
uint32_t intervals[MAX_INTERVALS][2];
int intervalCount = sizeof(raw_intervals) / sizeof(raw_intervals[0]);
uint32_t totalGlyphs = 0;
if (argc < 6 || argc % 2 == 1) {
printf(
"Usage: %s <regular.ttf> [-b <bold.ttf>] [-i <italic.ttf>] [-bi <bold-italic.ttf>] -p point_size -o <out.cpf "
"or out.h>\n",
argv[0]);
return 1;
}
regularFaceFile = argv[1];
for (int i = 2; i < argc; i += 2) {
if (strcmp(argv[i], "-b") == 0) {
// Bold font
boldFaceFile = argv[i + 1];
} else if (strcmp(argv[i], "-i") == 0) {
// Italic font
italicFaceFile = argv[i + 1];
} else if (strcmp(argv[i], "-bi") == 0) {
// Bold-Italic font
boldItalicFaceFile = argv[i + 1];
} else if (strcmp(argv[i], "-p") == 0) {
// Point size
size = atoi(argv[i + 1]);
} else if (strcmp(argv[i], "-o") == 0) {
// Output file
outputFile = argv[i + 1];
// output an H file?
bHFile = outputFile[strlen(outputFile) - 1] == 'H' || outputFile[strlen(outputFile) - 1] == 'h';
} else {
printf("Unknown argument: %s\n", argv[i]);
return 1;
}
}
if (!outputFile) {
printf("No output file specified\n");
return 1;
}
if (size <= 0) {
printf("Invalid point size: %d\n", size);
return 1;
}
size = size * FONT_SCALE_FACTOR;
bSmallFont = (size < 60) && SMALL_FONT_ENABLED == 1; // Glyph info can fit in signed 8-bit values
int fontVariants = 1; // Always at least one variant we treat as regular
if (boldFaceFile) fontVariants += 1;
if (italicFaceFile) fontVariants += 1;
if (boldItalicFaceFile) fontVariants += 1;
// Copy and sort/merge intervals
if (intervalCount > MAX_INTERVALS) {
printf("Error: too many intervals (max %d)\n", MAX_INTERVALS);
return 1;
}
for (i = 0; i < intervalCount; i++) {
intervals[i][0] = raw_intervals[i][0];
intervals[i][1] = raw_intervals[i][1];
}
intervalCount = sortAndMergeIntervals(intervals, intervalCount);
// Calculate total number of glyphs
for (i = 0; i < intervalCount; i++) {
totalGlyphs += intervals[i][1] - intervals[i][0] + 1;
}
totalGlyphs *= fontVariants;
printf("Processed intervals: %d, total glyphs: %u\n", intervalCount, totalGlyphs);
// Allocate memory for intervals
pIntervals = (CrossPointFontUnicodeInterval*)malloc(intervalCount * sizeof(CrossPointFontUnicodeInterval));
if (!pIntervals) {
printf("Error allocating memory for interval data\n");
return 1;
}
// Allocate memory for glyphs
if (bSmallFont) {
#if SMALL_FONT_ENABLED == 1
pSmallGlyphs = (CrossPointFontSmallGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontSmallGlyph));
if (!pSmallGlyphs) {
printf("Error allocating memory for glyph data\n");
return 1;
}
#else
printf("Small font has been disabled\n");
return 1;
#endif
} else {
pGlyphs = (CrossPointFontGlyph*)malloc(totalGlyphs * sizeof(CrossPointFontGlyph));
if (!pGlyphs) {
printf("Error allocating memory for glyph data\n");
return 1;
}
}
pBitmap = (uint8_t*)malloc(OUTBUF_SIZE); // Enough to hold the output
if (!pBitmap) {
printf("Error allocating memory for bitmap data\n");
return 1;
}
// Init FreeType lib, load font
if ((err = FT_Init_FreeType(&library))) {
printf("FreeType init error: %d", err);
return err;
}
// Use TrueType engine version 35, without subpixel rendering.
// This improves clarity of fonts since this library does not
// support rendering multiple levels of gray in a glyph.
// See https://github.com/adafruit/Adafruit-GFX-Library/issues/103
FT_UInt interpreter_version = TT_INTERPRETER_VERSION_35;
FT_Property_Set(library, "truetype", "interpreter-version", &interpreter_version);
if ((err = FT_New_Face(library, regularFaceFile, 0, &faceRegular))) {
printf("Font load error: %d\n", err);
FT_Done_FreeType(library);
return err;
}
if (italicFaceFile && (err = FT_New_Face(library, italicFaceFile, 0, &faceItalic))) {
printf("Font load error: %d\n", err);
FT_Done_FreeType(library);
return err;
}
if (boldFaceFile && (err = FT_New_Face(library, boldFaceFile, 0, &faceBold))) {
printf("Font load error: %d\n", err);
FT_Done_FreeType(library);
return err;
}
if (boldItalicFaceFile && (err = FT_New_Face(library, boldItalicFaceFile, 0, &faceBoldItalic))) {
printf("Font load error: %d\n", err);
FT_Done_FreeType(library);
return err;
}
// Shift the size left by 6 because the library uses '26dot6' fixed-point format
FT_Set_Char_Size(faceRegular, size << 6, 0, DPI, 0);
if (boldFaceFile) FT_Set_Char_Size(faceBold, size << 6, 0, DPI, 0);
if (italicFaceFile) FT_Set_Char_Size(faceItalic, size << 6, 0, DPI, 0);
if (boldItalicFaceFile) FT_Set_Char_Size(faceBoldItalic, size << 6, 0, DPI, 0);
// Build intervals with offsets and process glyphs
uint32_t glyph_index = 0;
for (int iInterval = 0; iInterval < intervalCount; iInterval++) {
const uint32_t intervalStart = intervals[iInterval][0];
const uint32_t intervalEnd = intervals[iInterval][1];
// Store interval with offset
pIntervals[iInterval].first = intervalStart;
pIntervals[iInterval].last = intervalEnd;
pIntervals[iInterval].offset = glyph_index;
// Process each glyph in this interval
// Load the codepoint for each style variant
for (uint32_t codePoint = intervalStart; codePoint <= intervalEnd; codePoint++) {
loadCodePoint(faceRegular, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
if (boldFaceFile) loadCodePoint(faceBold, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
if (italicFaceFile) loadCodePoint(faceItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
if (boldItalicFaceFile) loadCodePoint(faceBoldItalic, codePoint, pGlyphs, pBitmap, &glyph_index, &iOffset);
} // for each code point in interval
} // for each interval
// Try to create the output file
fOut = fopen(outputFile, "w+b");
if (!fOut) {
printf("Error creating output file: %s\n", outputFile);
return 1;
}
epdFontHeader.height = faceRegular->size->metrics.height >> 6;
epdFontHeader.ascender = faceRegular->size->metrics.ascender >> 6;
epdFontHeader.styles = 0b0001;
if (boldFaceFile) epdFontHeader.styles |= 0b0010;
if (italicFaceFile) epdFontHeader.styles |= 0b0100;
if (boldItalicFaceFile) epdFontHeader.styles |= 0b1000;
epdFontHeader.intervalCount = intervalCount;
epdFontHeader.glyphCount = totalGlyphs;
// Write the file header
if (bSmallFont) {
#if SMALL_FONT_ENABLED == 1
epdFontHeader.u16Marker = CPF_FONT_MARKER_SMALL;
if (faceRegular->size->metrics.height == 0) {
// No face height info, assume fixed width and get from a glyph.
epdFontHeader.height = pSmallGlyphs[0].height;
}
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
totalGlyphs * sizeof(CrossPointFontSmallGlyph) + iOffset;
if (bHFile) { // create an H file of hex values
StartHexFile(fOut, iLen, outputFile, size);
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
// Write the intervals
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
// Write the glyph table
AddHexBytes(fOut, pSmallGlyphs, sizeof(CrossPointFontSmallGlyph) * totalGlyphs, 0);
// Write the compressed bitmap data
AddHexBytes(fOut, pBitmap, iOffset, 1);
} else {
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
// Write the intervals
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
// Write the glyph table
fwrite(pSmallGlyphs, 1, totalGlyphs * sizeof(CrossPointFontSmallGlyph), fOut);
// Write the compressed bitmap data
fwrite(pBitmap, 1, iOffset, fOut);
}
#else
printf("Small font has been disabled\n");
return 1;
#endif
} else {
epdFontHeader.u16Marker = CPF_FONT_MARKER;
if (faceRegular->size->metrics.height == 0) {
// No face height info, assume fixed width and get from a glyph.
epdFontHeader.height = pGlyphs[0].height;
}
iLen = sizeof(CrossPointFontHeader) + intervalCount * sizeof(CrossPointFontUnicodeInterval) +
totalGlyphs * sizeof(CrossPointFontGlyph) + iOffset;
if (bHFile) { // create an H file of hex values
StartHexFile(fOut, iLen, outputFile, size);
AddHexBytes(fOut, &epdFontHeader, sizeof(CrossPointFontHeader), 0);
// Write the intervals
AddHexBytes(fOut, pIntervals, sizeof(CrossPointFontUnicodeInterval) * intervalCount, 0);
// Write the glyph table
AddHexBytes(fOut, pGlyphs, sizeof(CrossPointFontGlyph) * totalGlyphs, 0);
// Write the compressed bitmap data
AddHexBytes(fOut, pBitmap, iOffset, 1);
} else {
fwrite(&epdFontHeader, 1, sizeof(CrossPointFontHeader), fOut);
// Write the intervals
fwrite(pIntervals, 1, intervalCount * sizeof(CrossPointFontUnicodeInterval), fOut);
// Write the glyph table
fwrite(pGlyphs, 1, totalGlyphs * sizeof(CrossPointFontGlyph), fOut);
// Write the compressed bitmap data
fwrite(pBitmap, 1, iOffset, fOut);
}
} // large fonts
fflush(fOut);
fclose(fOut); // done!
FT_Done_FreeType(library);
printf("Success!\nFont file size: %d bytes (%d glyphs)\n", iLen, totalGlyphs);
return 0;
} /* main() */
#endif

View File

@@ -1,72 +0,0 @@
#include "EpdFont.h"
#include <Utf8.h>
inline int min(const int a, const int b) { return a < b ? a : b; }
inline int max(const int a, const int b) { return a < b ? b : a; }
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
int* maxY) const {
*minX = startX;
*minY = startY;
*maxX = startX;
*maxY = startY;
if (*string == '\0') {
return;
}
int cursorX = startX;
const int cursorY = startY;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) {
// TODO: Replace with fallback glyph property?
glyph = getGlyph('?');
}
if (!glyph) {
// TODO: Better handle this?
continue;
}
*minX = min(*minX, cursorX + glyph->left);
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
*minY = min(*minY, cursorY + glyph->top - glyph->height);
*maxY = max(*maxY, cursorY + glyph->top);
cursorX += glyph->advanceX;
}
}
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
int minX = 0, minY = 0, maxX = 0, maxY = 0;
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
*w = maxX - minX;
*h = maxY - minY;
}
bool EpdFont::hasPrintableChars(const char* string) const {
int w = 0, h = 0;
getTextDimensions(string, &w, &h);
return w > 0 || h > 0;
}
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const EpdUnicodeInterval* intervals = data->intervals;
for (int i = 0; i < data->intervalCount; i++) {
const EpdUnicodeInterval* interval = &intervals[i];
if (cp >= interval->first && cp <= interval->last) {
return &data->glyph[interval->offset + (cp - interval->first)];
}
if (cp < interval->first) {
return nullptr;
}
}
return nullptr;
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include "EpdFontData.h"
class EpdFont {
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
public:
const EpdFontData* data;
explicit EpdFont(const EpdFontData* data) : data(data) {}
~EpdFont() = default;
void getTextDimensions(const char* string, int* w, int* h) const;
bool hasPrintableChars(const char* string) const;
const EpdGlyph* getGlyph(uint32_t cp) const;
};

View File

@@ -1,34 +0,0 @@
// From
// https://github.com/vroland/epdiy/blob/c61e9e923ce2418150d54f88cea5d196cdc40c54/src/epd_internals.h
#pragma once
#include <cstdint>
/// Font data stored PER GLYPH
typedef struct {
uint8_t width; ///< Bitmap dimensions in pixels
uint8_t height; ///< Bitmap dimensions in pixels
uint8_t advanceX; ///< Distance to advance cursor (x axis)
int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data.
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
} EpdGlyph;
/// Glyph interval structure
typedef struct {
uint32_t first; ///< The first unicode code point of the interval
uint32_t last; ///< The last unicode code point of the interval
uint32_t offset; ///< Index of the first code point into the glyph array
} EpdUnicodeInterval;
/// Data stored for FONT AS A WHOLE
typedef struct {
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
const EpdGlyph* glyph; ///< Glyph array
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
uint32_t intervalCount; ///< Number of unicode intervals.
uint8_t advanceY; ///< Newline distance (y axis)
int ascender; ///< Maximal height of a glyph above the base line
int descender; ///< Maximal height of a glyph below the base line
} EpdFontData;

View File

@@ -1,37 +0,0 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
if (style == BOLD && bold) {
return bold;
}
if (style == ITALIC && italic) {
return italic;
}
if (style == BOLD_ITALIC) {
if (boldItalic) {
return boldItalic;
}
if (bold) {
return bold;
}
if (italic) {
return italic;
}
}
return regular;
}
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyle style) const {
getFont(style)->getTextDimensions(string, w, h);
}
bool EpdFontFamily::hasPrintableChars(const char* string, const EpdFontStyle style) const {
return getFont(style)->hasPrintableChars(string);
}
const EpdFontData* EpdFontFamily::getData(const EpdFontStyle style) const { return getFont(style)->data; }
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const EpdFontStyle style) const {
return getFont(style)->getGlyph(cp);
};

View File

@@ -1,24 +0,0 @@
#pragma once
#include "EpdFont.h"
enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC };
class EpdFontFamily {
const EpdFont* regular;
const EpdFont* bold;
const EpdFont* italic;
const EpdFont* boldItalic;
const EpdFont* getFont(EpdFontStyle style) const;
public:
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const;
bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const;
const EpdFontData* getData(EpdFontStyle style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const;
};

View File

@@ -1,504 +0,0 @@
/**
* generated by fontconvert.py
* name: babyblue
* size: 8
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t babyblueBitmaps[3140] = {
0xFF, 0xFF, 0x30, 0xFF, 0xFF, 0xF0, 0x36, 0x1B, 0x0D, 0x9F, 0xFF, 0xF9, 0xB3, 0xFF, 0xFF, 0x36, 0x1B, 0x0D, 0x80,
0x18, 0x18, 0x7E, 0xFF, 0xDB, 0xD8, 0xFE, 0x7F, 0x9B, 0xDB, 0xFF, 0x7E, 0x18, 0x00, 0x71, 0x9F, 0x73, 0x6C, 0x6F,
0x8F, 0xE0, 0xFF, 0x83, 0xF8, 0xFB, 0x1B, 0x63, 0x7C, 0xC7, 0x00, 0x38, 0x3E, 0x1B, 0x0D, 0x87, 0xC3, 0xEF, 0xBF,
0x8E, 0xCF, 0x7F, 0x1E, 0xC0, 0xFF, 0x37, 0xEC, 0xCC, 0xCC, 0xCC, 0xCC, 0x67, 0x20, 0xCE, 0x73, 0x33, 0x33, 0x33,
0x33, 0x6C, 0x80, 0x32, 0xFF, 0xDE, 0xFE, 0x30, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0xBF, 0x00, 0xFF,
0xC0, 0x30, 0x0C, 0x31, 0xC6, 0x18, 0xE3, 0x1C, 0x63, 0x8C, 0x00, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xC7, 0x7E, 0x3C, 0x19, 0xDF, 0xFD, 0x8C, 0x63, 0x18, 0xC6, 0x3C, 0x7E, 0xE7, 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70,
0xFE, 0xFF, 0x7E, 0xFF, 0xC3, 0x07, 0x3E, 0x3E, 0x07, 0x03, 0x83, 0xFF, 0x7E, 0x0E, 0x1E, 0x3E, 0x76, 0xE6, 0xFF,
0xFF, 0x06, 0x06, 0x06, 0x06, 0x7F, 0xFF, 0x06, 0x0F, 0xDF, 0xC1, 0x83, 0x87, 0xFD, 0xF0, 0x3E, 0x7F, 0xE3, 0xC0,
0xFC, 0xFE, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0xFF, 0xFF, 0xC0, 0xE0, 0xE0, 0x60, 0x70, 0x30, 0x18, 0x1C, 0x0C, 0x06,
0x00, 0x3C, 0x7E, 0x66, 0x66, 0x7E, 0x7E, 0xE7, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7F,
0x3F, 0x07, 0x3E, 0x7C, 0xB0, 0x03, 0xB0, 0x03, 0xF8, 0x06, 0x3D, 0xF7, 0x8F, 0x0F, 0x87, 0x03, 0xFF, 0xFF, 0x00,
0xFF, 0xFF, 0x81, 0xE1, 0xF0, 0xF1, 0xEF, 0xBC, 0x40, 0x38, 0xFB, 0xBE, 0x30, 0xE3, 0x8E, 0x18, 0x30, 0x00, 0xC0,
0x0F, 0xE0, 0x7F, 0xC3, 0x83, 0x9D, 0xE7, 0xEF, 0xCF, 0x73, 0x3D, 0x8C, 0xF6, 0x33, 0xD8, 0xDB, 0x3F, 0xC6, 0x7E,
0x0C, 0x03, 0x18, 0x18, 0x7F, 0xE0, 0xFF, 0x00, 0x38, 0xFB, 0xBE, 0x3C, 0x7F, 0xFF, 0xE3, 0xC7, 0x8C, 0xFC, 0xFE,
0xC7, 0xC7, 0xFE, 0xFE, 0xC7, 0xC7, 0xFE, 0xFC, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01, 0x83, 0x7F, 0x9F,
0x80, 0xFC, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xFD,
0xFC, 0xFF, 0xFF, 0x06, 0x0F, 0xDF, 0xB0, 0x60, 0xC1, 0x80, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x7E, 0x3F, 0x07, 0x83,
0x7F, 0x9F, 0x80, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xF0, 0x0C, 0x30, 0xC3,
0x0C, 0x38, 0xF3, 0xFD, 0xE0, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0xC1, 0x83, 0x06, 0x0C,
0x18, 0x30, 0x60, 0xFD, 0xFC, 0xE1, 0xF8, 0x7F, 0x3F, 0xCF, 0xFF, 0xF7, 0xBD, 0xEF, 0x7B, 0xCC, 0xF3, 0x30, 0xC3,
0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F, 0x07, 0x87, 0x7F,
0x1F, 0x00, 0xFE, 0xFF, 0xC3, 0xC3, 0xFF, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0x3E, 0x3F, 0xB8, 0xF8, 0x3C, 0x1E, 0x0F,
0x37, 0x9F, 0x7F, 0x1F, 0xC0, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x7D, 0xFF, 0x1F, 0x87,
0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0xC3, 0xC3, 0xC3, 0xE7, 0x66, 0x7E, 0x3C, 0x3C, 0x3C, 0x18, 0x83, 0x0F, 0x0C,
0x3C, 0x30, 0xF1, 0xE7, 0x67, 0x99, 0xBF, 0xE3, 0xCF, 0x0F, 0x3C, 0x3C, 0xF0, 0x61, 0x80, 0x80, 0xF8, 0x77, 0x38,
0xFC, 0x1E, 0x07, 0x83, 0xF1, 0xCE, 0xE1, 0xB0, 0x30, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18,
0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0xFF, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xFE, 0x81, 0xC1,
0x83, 0x83, 0x83, 0x06, 0x06, 0x0C, 0x0C, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x33, 0xFE, 0x18, 0x3C, 0x7E, 0x66, 0xE7,
0xC3, 0x83, 0xFF, 0xFF, 0x9D, 0x80, 0x7D, 0xFE, 0x1B, 0xFF, 0xF8, 0xFF, 0xBF, 0xC1, 0x83, 0xE7, 0xEC, 0xF8, 0xF1,
0xE7, 0xFD, 0xF0, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x06, 0x0C, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C,
0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3B, 0xD9, 0xFF, 0xB1, 0x8C, 0x63, 0x18, 0x3E, 0xFF, 0x9E, 0x3C,
0x78, 0xDF, 0x9F, 0x06, 0x0D, 0xF3, 0xC0, 0xC3, 0x0F, 0xBF, 0xEF, 0x3C, 0xF3, 0xCF, 0x30, 0xFB, 0xFF, 0xF0, 0x6D,
0x36, 0xDB, 0x6D, 0xBD, 0x00, 0xC3, 0x0C, 0xF7, 0xFB, 0xCE, 0x3C, 0xDB, 0x30, 0xFF, 0xFF, 0xF0, 0x7F, 0xBF, 0xFC,
0xCF, 0x33, 0xCC, 0xF3, 0x3C, 0xCF, 0x33, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C, 0xF3, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7,
0x7E, 0x3C, 0x79, 0xFB, 0x3E, 0x3C, 0x79, 0xFF, 0x7C, 0xC1, 0x82, 0x00, 0x3C, 0xFF, 0x9E, 0x3C, 0x78, 0xDF, 0x9F,
0x06, 0x0C, 0x10, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x63, 0x3D, 0xE6, 0x31,
0x8C, 0x71, 0xC0, 0x8F, 0x3C, 0xF3, 0xCF, 0x3F, 0xDE, 0x83, 0xC3, 0xC7, 0x66, 0x66, 0x6E, 0x3C, 0x18, 0x80, 0xF3,
0x3D, 0xFD, 0xFE, 0x7F, 0x9F, 0xE3, 0x30, 0xCC, 0x83, 0xC7, 0x6E, 0x3C, 0x38, 0x7C, 0xE6, 0xC3, 0x83, 0xC7, 0x66,
0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xFF, 0xFC, 0x71, 0xC7, 0x1C, 0x3F, 0x7F, 0x19, 0xCC, 0x63, 0x3B,
0x98, 0x61, 0x8C, 0x63, 0x1C, 0x40, 0xFF, 0xFF, 0xFF, 0xF8, 0x83, 0x87, 0x0C, 0x30, 0xE1, 0xC7, 0x38, 0xC3, 0x0C,
0x63, 0x08, 0x00, 0x79, 0xFF, 0xE3, 0xC0, 0xF2, 0xFF, 0xFE, 0x18, 0x30, 0xF3, 0xFF, 0xFB, 0x36, 0x6E, 0x7E, 0x78,
0x60, 0xC1, 0x00, 0x3C, 0x7E, 0x66, 0x60, 0xFC, 0xFC, 0x30, 0x72, 0xFF, 0xFE, 0x83, 0xFF, 0x7E, 0x66, 0x66, 0x7E,
0xFE, 0x83, 0x83, 0xE7, 0x7E, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0x18, 0x18, 0xFF, 0xFC, 0xBF, 0xF8, 0x3C, 0x7E, 0x66,
0x7E, 0x7C, 0xEE, 0xC7, 0xC3, 0x77, 0x3E, 0x0C, 0x66, 0x66, 0x7C, 0x38, 0x9E, 0xE6, 0x3F, 0x1F, 0xEF, 0xFF, 0xFF,
0xF3, 0xFC, 0x3F, 0x3F, 0xFF, 0xDF, 0xDF, 0xE3, 0xF0, 0x77, 0xFF, 0xFF, 0xBC, 0x36, 0xFF, 0xF6, 0xCD, 0xCD, 0x8D,
0x80, 0xFF, 0xFF, 0x03, 0x03, 0x03, 0x3F, 0x1F, 0xEF, 0xFF, 0xFB, 0xF6, 0xFF, 0xBF, 0xEF, 0xDB, 0xF7, 0xDF, 0xE3,
0xF0, 0xFF, 0xFF, 0x6F, 0xFF, 0x60, 0x18, 0x18, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0xFE, 0xFF, 0x77, 0xE6, 0x77, 0x73,
0xFF, 0x77, 0xEF, 0x7F, 0xB8, 0x7F, 0x00, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xFE, 0xC3, 0x0C, 0x20, 0x7F, 0xFF, 0xFE,
0xFE, 0xFE, 0x7E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x16, 0xB0, 0x63, 0xEC, 0x37, 0xFB, 0x33, 0x30, 0x7B,
0xFC, 0xF3, 0xFD, 0xE0, 0x99, 0xF9, 0xF9, 0xB7, 0xFF, 0xB6, 0x00, 0x30, 0x07, 0x03, 0xF0, 0x7B, 0x0E, 0x31, 0xC3,
0x38, 0x37, 0x60, 0xEE, 0x1D, 0xE3, 0xBF, 0x33, 0xF6, 0x06, 0x30, 0x67, 0x0E, 0xF1, 0xCB, 0x38, 0x37, 0x03, 0xEF,
0x1D, 0xF3, 0x9F, 0x70, 0xEE, 0x0E, 0xC1, 0xF0, 0x70, 0x6F, 0x8E, 0xB9, 0xCB, 0xB8, 0xFF, 0x07, 0xE6, 0x1C, 0xE3,
0x9E, 0x73, 0xFE, 0x3F, 0xC0, 0x60, 0x30, 0x60, 0xC1, 0x87, 0x1C, 0x30, 0x60, 0xC6, 0xD9, 0xE1, 0x80, 0x30, 0x70,
0x61, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x18, 0x70, 0xC1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF,
0x1E, 0x3C, 0x60, 0x38, 0xF9, 0xB1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x3C, 0xF9, 0xE1, 0xC7,
0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xD8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30,
0x38, 0xF9, 0xF1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x0F, 0xF8, 0x7F, 0xC7, 0xC0, 0x36, 0x03,
0xB0, 0x19, 0xF9, 0xFF, 0xCF, 0xE0, 0xE3, 0x06, 0x1F, 0xB0, 0xFE, 0x3F, 0x3F, 0xF8, 0x78, 0x0C, 0x06, 0x03, 0x01,
0x83, 0x7F, 0x9F, 0x86, 0x01, 0x83, 0x81, 0x80, 0x30, 0x70, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF,
0xE0, 0x0C, 0x38, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x18, 0x78, 0xF7, 0xFF, 0xF8, 0x30,
0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x9D, 0x36,
0xDB, 0x6D, 0xB6, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0x6F, 0xB6, 0x66, 0x66, 0x66, 0x66, 0x60, 0xBB, 0x66, 0x66, 0x66,
0x66, 0x66, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x1E, 0x3E, 0x3C, 0xC3, 0xE3,
0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x0E, 0x03, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0,
0xF0, 0xEF, 0xE3, 0xE0, 0x0C, 0x0E, 0x06, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0,
0x1C, 0x1F, 0x0D, 0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x1F, 0x0F, 0x07,
0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x1B, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07,
0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x8F, 0xF7, 0x9E, 0xFF, 0x30, 0x1F, 0xCF, 0xF7, 0x3D, 0x9F, 0x6E, 0xDF, 0x37,
0x8D, 0xC7, 0xFF, 0xB7, 0xC0, 0x30, 0x38, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
0x1C, 0x18, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x0C,
0x1C, 0x18, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0xC0, 0xFC, 0xFE, 0xC7, 0xC3, 0xC7, 0xFE,
0xFC, 0xC0, 0xC0, 0x38, 0xFB, 0xB6, 0x6D, 0xDB, 0x37, 0x67, 0xE7, 0xFF, 0x70, 0x30, 0x70, 0x63, 0xEF, 0xF0, 0xDF,
0xFF, 0xC7, 0xFD, 0xF8, 0x0C, 0x38, 0x63, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x18, 0x78, 0xF3, 0xEF, 0xF0,
0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0xF9, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF, 0xC7, 0xFD, 0xF8, 0x3C, 0x79, 0xF7, 0xF8,
0x6F, 0xFF, 0xE3, 0xFE, 0xFC, 0x18, 0x78, 0xF0, 0xC7, 0xDF, 0xE1, 0xBF, 0xFF, 0x8F, 0xFB, 0xF0, 0x7F, 0xEF, 0xFF,
0x86, 0x37, 0xFF, 0xFF, 0xFC, 0x61, 0xFF, 0xF7, 0xFE, 0x3C, 0xFF, 0x9E, 0x0C, 0x18, 0x9F, 0x9E, 0x18, 0x18, 0xE1,
0x80, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xFF,
0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x3C, 0x3C, 0x3C, 0x7E,
0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x9D, 0xA6, 0xDB, 0x6D, 0x80, 0x7F, 0x4D, 0xB6, 0xDB, 0x00, 0x6F, 0xB4, 0x66,
0x66, 0x66, 0x60, 0xBB, 0x46, 0x66, 0x66, 0x66, 0x3E, 0x3E, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
0x3D, 0xF7, 0x9E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x30, 0x38, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E,
0x3C, 0x0C, 0x1C, 0x18, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x3C, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
0xC3, 0xC7, 0x7E, 0x3C, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3C, 0x3C, 0x3C, 0x7E,
0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x18, 0xFF, 0xFF, 0x10, 0x18, 0x3F, 0x7F, 0xEF, 0xDF, 0xFB, 0xF7, 0xFE,
0xFC, 0x61, 0xC3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0xE3, 0x23, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80,
0x31, 0xE7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x0C, 0x1C,
0x18, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0xC1, 0x83, 0xE7, 0xEE, 0xF8, 0xF1, 0xE7,
0xFD, 0xF3, 0x06, 0x08, 0x00, 0x3C, 0x3C, 0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x7C,
0xF8, 0xE3, 0xEE, 0xF8, 0xF1, 0xFF, 0xFF, 0x8F, 0x1E, 0x30, 0x7C, 0xF9, 0xF7, 0xF8, 0x6F, 0xFF, 0xE3, 0xFE, 0xFC,
0x6C, 0xF8, 0xE1, 0xC7, 0xDD, 0xF1, 0xE3, 0xFF, 0xFF, 0x1E, 0x3C, 0x60, 0x6C, 0xF8, 0xE3, 0xEF, 0xF0, 0xDF, 0xFF,
0xC7, 0xFD, 0xF8, 0x38, 0x7C, 0xEE, 0xC6, 0xC6, 0xFE, 0xFE, 0xC6, 0xC6, 0xC6, 0x0C, 0x0C, 0x0F, 0x07, 0x7C, 0xFE,
0x86, 0x7E, 0xFE, 0xC6, 0xFE, 0x7E, 0x06, 0x0C, 0x0F, 0x0F, 0x0C, 0x0E, 0x06, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80,
0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x0C, 0x38, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x0C, 0x0F, 0x07,
0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x6F, 0xF3, 0xF0, 0x18, 0x78, 0xF1, 0xE7, 0xFC, 0xF0, 0x60,
0xC4, 0xFC, 0xF0, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x01, 0x80, 0xC0, 0x60, 0xDF, 0xE7, 0xE0, 0x18, 0x30,
0xF3, 0xFE, 0x78, 0x30, 0x62, 0x7E, 0x78, 0x1E, 0x0F, 0x03, 0x07, 0xE7, 0xFF, 0x0F, 0x01, 0x80, 0xC0, 0x60, 0x30,
0x6F, 0xF3, 0xF0, 0x3C, 0x78, 0x61, 0xE7, 0xFC, 0xF0, 0x60, 0xC4, 0xFC, 0xF0, 0x78, 0x78, 0x30, 0xFC, 0xFE, 0xC7,
0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFE, 0xFC, 0x01, 0x83, 0xC1, 0xE7, 0xC7, 0xE7, 0x33, 0x19, 0x8C, 0xC6, 0x3F, 0x0F,
0x80, 0x7E, 0x3F, 0x98, 0xEC, 0x3F, 0xDF, 0xED, 0x86, 0xC7, 0x7F, 0x3F, 0x00, 0x06, 0x1F, 0x1F, 0x3E, 0x7E, 0xE6,
0xC6, 0xC6, 0xC6, 0x7E, 0x3E, 0x3C, 0x7B, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1, 0x83, 0xF7, 0xF0, 0x3C, 0x3C, 0x3C,
0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x66, 0xFC, 0xF7, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0,
0x66, 0x7E, 0x3C, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x18, 0x33, 0xFF, 0xFC, 0x18, 0x3F, 0x7E, 0xC1,
0x83, 0xF7, 0xF0, 0x18, 0x18, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0xFE, 0xFE, 0xC0, 0xC0, 0xFC, 0xFC,
0xC0, 0xC0, 0xFC, 0xFE, 0x06, 0x0C, 0x0F, 0x0F, 0x3C, 0x7E, 0xE7, 0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x06, 0x0C, 0x0F,
0x07, 0x3C, 0x78, 0x67, 0xFF, 0xF8, 0x30, 0x7E, 0xFD, 0x83, 0x07, 0xEF, 0xE0, 0x3C, 0x3C, 0x18, 0x3C, 0x7E, 0xE7,
0xFF, 0xFF, 0xC0, 0x7E, 0x3F, 0x0C, 0x0F, 0x07, 0x87, 0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3,
0xF0, 0x18, 0x78, 0xF1, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6, 0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x36, 0x1F, 0x07, 0x07,
0xE7, 0xFF, 0x0F, 0x01, 0x8F, 0xC7, 0xE0, 0xF0, 0x6F, 0xF3, 0xF0, 0x36, 0x7C, 0x71, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x0C, 0x06, 0x0F, 0xCF, 0xFE, 0x1E, 0x03, 0x1F, 0x8F, 0xC1, 0xE0, 0xDF, 0xE7,
0xE0, 0x18, 0x30, 0xFB, 0xFE, 0x78, 0xF1, 0xE3, 0x7E, 0x7C, 0x18, 0x37, 0xCF, 0x00, 0x3F, 0x3F, 0xF8, 0x78, 0x0C,
0x7E, 0x3F, 0x07, 0x83, 0x7F, 0x9F, 0x83, 0x00, 0xC1, 0xC0, 0xE0, 0x18, 0x30, 0x61, 0xF7, 0xFC, 0xF1, 0xE3, 0xC6,
0xFC, 0xF8, 0x30, 0x6F, 0x9E, 0x00, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3,
0x31, 0xE7, 0xB0, 0xC3, 0xEF, 0xFB, 0xCF, 0x3C, 0xF3, 0xCC, 0x61, 0xBF, 0xFF, 0xFD, 0x86, 0x7F, 0x9F, 0xE6, 0x19,
0x86, 0x61, 0x98, 0x60, 0x61, 0xF3, 0xE3, 0xE7, 0xEE, 0xD9, 0xB3, 0x66, 0xCD, 0x98, 0x7F, 0xEC, 0xC6, 0x31, 0x8C,
0x63, 0x18, 0xC6, 0x00, 0x7F, 0xEC, 0x86, 0x31, 0x8C, 0x63, 0x18, 0xFF, 0x66, 0x66, 0x66, 0x66, 0x66, 0xFF, 0x46,
0x66, 0x66, 0x66, 0x9F, 0xDC, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x00, 0x9F, 0xDC, 0x86, 0x31, 0x8C, 0x63, 0x18,
0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0x66, 0x46, 0x66, 0x66, 0x66, 0x6C, 0xF6, 0xEF, 0xFF, 0xFF, 0xBF, 0xFF,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xE3, 0xF3, 0xFF, 0xDE, 0xDE, 0xE7, 0xBD, 0xEF, 0x7B, 0xDE, 0xC6, 0x33, 0x10,
0x0C, 0x3C, 0x78, 0x60, 0xC1, 0x83, 0x06, 0x0D, 0x1B, 0x37, 0xE7, 0x80, 0x6F, 0xB4, 0x66, 0x66, 0x66, 0x66, 0x6C,
0x80, 0xC3, 0xC7, 0xCE, 0xDC, 0xF8, 0xF8, 0xDC, 0xCC, 0xC6, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0xC3, 0x0C, 0xF7, 0xFB,
0xCE, 0x3C, 0xDB, 0x37, 0x0E, 0x71, 0x80, 0x8F, 0x7F, 0xBC, 0xE3, 0xCD, 0xB3, 0x30, 0xE1, 0x86, 0x0C, 0x18, 0x30,
0x60, 0xC1, 0x83, 0x07, 0xEF, 0xE0, 0x7A, 0x6D, 0xB6, 0xDB, 0x6C, 0xC1, 0x83, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xFD,
0xFD, 0xC1, 0xC7, 0x0C, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0xEF, 0xEC, 0x0D, 0x9B, 0x36, 0x6C, 0x18, 0x30, 0x60,
0xC1, 0xFB, 0xF8, 0x3F, 0xFF, 0xCC, 0xCC, 0xCC, 0xC0, 0xC1, 0x83, 0x06, 0x0C, 0xD9, 0xB0, 0x60, 0xFD, 0xFC, 0xCC,
0xCC, 0xFF, 0xCC, 0xCC, 0x60, 0x60, 0x78, 0x78, 0xF0, 0xE0, 0x60, 0x60, 0x7E, 0x7F, 0x66, 0x67, 0xFE, 0x66, 0x66,
0x0C, 0x1C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x18, 0x63, 0x8C, 0x7B, 0xFC, 0xF3,
0xCF, 0x3C, 0xF3, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x7B, 0xFC,
0xF3, 0xCF, 0x3C, 0xF3, 0x70, 0xE7, 0x18, 0x3C, 0x3C, 0x18, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7,
0xC3, 0x79, 0xE3, 0x1E, 0xFF, 0x3C, 0xF3, 0xCF, 0x3C, 0xC0, 0x81, 0x83, 0x05, 0xE7, 0xEC, 0xD9, 0xB3, 0x66, 0xCD,
0x98, 0xC3, 0xE3, 0xF3, 0xF3, 0xFB, 0xDF, 0xCF, 0xCF, 0xC7, 0xC3, 0x03, 0x03, 0x03, 0x7B, 0xFC, 0xF3, 0xCF, 0x3C,
0xF3, 0x0C, 0x31, 0x84, 0x3E, 0x1F, 0x0F, 0x8F, 0xEE, 0x3E, 0x0F, 0x07, 0x83, 0xC1, 0xE1, 0xDF, 0xC7, 0xC0, 0x3C,
0x3C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x36, 0x1F, 0x07, 0x07, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1,
0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x36, 0x3E, 0x1C, 0x3C, 0x7E, 0xE7, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x1B, 0x1F, 0x8D,
0x87, 0xC7, 0xF7, 0x1F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0xEF, 0xE3, 0xE0, 0x1E, 0x3E, 0x3C, 0x3C, 0x7E, 0xE7, 0xC3,
0xC3, 0xC7, 0x7E, 0x3C, 0x3F, 0xFB, 0xFF, 0xF9, 0xC1, 0x86, 0x0C, 0x3F, 0x61, 0xFB, 0x0C, 0x18, 0x60, 0xC7, 0x03,
0xFF, 0x0F, 0xFC, 0x3D, 0xE3, 0xFF, 0xB9, 0xCF, 0x87, 0xFC, 0x3F, 0xE3, 0x85, 0xFF, 0xE7, 0xBE, 0x18, 0x38, 0x30,
0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x33, 0x98, 0xEF, 0xEF, 0x18, 0xC6, 0x30, 0xFE, 0xFF,
0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0x38, 0x1C, 0x38, 0x30, 0x77, 0xF7, 0x8C, 0x63, 0x18, 0xE3, 0xB9,
0x80, 0x3C, 0x3C, 0x18, 0xFE, 0xFF, 0xC3, 0xC7, 0xFE, 0xFE, 0xC7, 0xC3, 0xC3, 0xC3, 0xF7, 0x98, 0xEF, 0xEF, 0x18,
0xC6, 0x30, 0x18, 0x70, 0xC3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x70, 0xC3, 0xEF, 0xF8,
0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x18, 0x78, 0xF3, 0xEF, 0xF8, 0xFC, 0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x18, 0x78,
0xF3, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0x7D, 0xFF, 0x1F, 0x87, 0xC3, 0xC1, 0xC3, 0xFE, 0xF8, 0xC0, 0xC7,
0x0C, 0x00, 0x7D, 0xFF, 0x1F, 0xE7, 0xF0, 0xFF, 0xBE, 0x18, 0x18, 0xE1, 0x80, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFC,
0x3E, 0x1E, 0x0E, 0x1F, 0xF7, 0xC0, 0x3C, 0x78, 0x63, 0xEF, 0xF8, 0xFF, 0x3F, 0x87, 0xFD, 0xF0, 0xFF, 0xFF, 0xC6,
0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x01, 0x83, 0x81, 0x80, 0x63, 0x3D, 0xE6, 0x31, 0x8C, 0x71,
0xCC, 0x37, 0x30, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0D, 0xB6, 0xFF,
0xF1, 0x86, 0x18, 0x61, 0xC3, 0x80, 0xFF, 0xFF, 0x18, 0x18, 0x7E, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x63, 0x3D, 0xE6,
0x73, 0xCC, 0x71, 0xC0, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7,
0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x3C, 0x3C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C,
0x79, 0xE8, 0xF3, 0xCF, 0x3C, 0xF3, 0xFD, 0xE0, 0x36, 0x3E, 0x1C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7,
0x7E, 0x3C, 0x6D, 0xF3, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x18, 0x3C, 0x3C, 0xDB, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x31, 0xE7, 0xAF, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0x1E, 0x3E, 0x3C, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x3D, 0xF7, 0xA3, 0xCF, 0x3C, 0xF3, 0xCF, 0xF7, 0x80, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0x7E, 0x3C, 0x18, 0x30, 0x3C, 0x1C, 0x8D, 0x9B, 0x36, 0x6C, 0xD9, 0xBF, 0x3E,
0x0C, 0x30, 0x78, 0x70, 0x03, 0x00, 0x1E, 0x00, 0x78, 0x20, 0xC3, 0xC3, 0x0F, 0x0C, 0x3C, 0x79, 0xD9, 0xE6, 0x6F,
0xF8, 0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x18, 0x60, 0x0C, 0x07, 0x81, 0xE2, 0x03, 0xCC, 0xF7, 0xF7, 0xF9, 0xFE, 0x7F,
0x8C, 0xC3, 0x30, 0x18, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x3C,
0x83, 0xC7, 0x66, 0x6E, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x70, 0x60, 0x3C, 0x3C, 0xC3, 0xC3, 0xE7, 0x7E, 0x3C, 0x18,
0x18, 0x18, 0x18, 0x18, 0x0C, 0x1C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE, 0xFF, 0x0C, 0x38,
0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x0C, 0x0C, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xFE,
0xFF, 0x18, 0x33, 0xFF, 0xF1, 0xC7, 0x1C, 0x70, 0xFD, 0xFC, 0x3C, 0x3C, 0x18, 0xFF, 0xFF, 0x07, 0x0E, 0x1C, 0x38,
0x70, 0xE0, 0xFE, 0xFF, 0x3C, 0x78, 0x67, 0xFF, 0xE3, 0x8E, 0x38, 0xE1, 0xFB, 0xF8, 0x1E, 0x3F, 0x73, 0xFE, 0xFE,
0xFE, 0xFE, 0x62, 0x3F, 0x1E,
};
static const EpdGlyph babyblueGlyphs[] = {
{0, 0, 5, 0, 0, 0, 0}, //
{2, 10, 3, 1, 10, 3, 0}, // !
{4, 5, 5, 1, 11, 3, 3}, // "
{9, 11, 10, 0, 11, 13, 6}, // #
{8, 14, 9, 0, 12, 14, 19}, // $
{11, 11, 13, 1, 11, 16, 33}, // %
{9, 11, 11, 1, 11, 13, 49}, // &
{2, 4, 3, 1, 11, 1, 62}, // '
{4, 15, 5, 1, 11, 8, 63}, // (
{4, 15, 5, 1, 11, 8, 71}, // )
{6, 6, 6, 0, 11, 5, 79}, // *
{8, 8, 9, 0, 9, 8, 84}, // +
{2, 5, 3, 1, 3, 2, 92}, // ,
{5, 2, 5, 0, 5, 2, 94}, // -
{2, 2, 3, 1, 2, 1, 96}, // .
{6, 11, 6, 0, 11, 9, 97}, // /
{8, 11, 9, 0, 11, 11, 106}, // 0
{5, 11, 6, 1, 11, 7, 117}, // 1
{8, 11, 9, 0, 11, 11, 124}, // 2
{8, 11, 10, 1, 11, 11, 135}, // 3
{8, 11, 9, 0, 11, 11, 146}, // 4
{7, 11, 9, 1, 11, 10, 157}, // 5
{8, 11, 9, 0, 11, 11, 167}, // 6
{9, 11, 10, 0, 11, 13, 178}, // 7
{8, 11, 9, 0, 11, 11, 191}, // 8
{8, 11, 9, 0, 11, 11, 202}, // 9
{2, 8, 3, 1, 8, 2, 213}, // :
{2, 11, 3, 1, 8, 3, 215}, // ;
{7, 8, 9, 1, 9, 7, 218}, // <
{8, 5, 9, 0, 8, 5, 225}, // =
{7, 8, 7, 0, 9, 7, 230}, // >
{7, 11, 9, 1, 11, 10, 237}, // ?
{14, 15, 15, 0, 11, 27, 247}, // @
{7, 10, 9, 1, 10, 9, 274}, // A
{8, 10, 10, 1, 10, 10, 283}, // B
{9, 10, 11, 1, 10, 12, 293}, // C
{8, 10, 10, 1, 10, 10, 305}, // D
{7, 10, 9, 1, 10, 9, 315}, // E
{7, 10, 9, 1, 10, 9, 324}, // F
{9, 10, 11, 1, 10, 12, 333}, // G
{8, 10, 10, 1, 10, 10, 345}, // H
{2, 10, 3, 1, 10, 3, 355}, // I
{6, 10, 6, 0, 10, 8, 358}, // J
{8, 10, 10, 1, 10, 10, 366}, // K
{7, 10, 9, 1, 10, 9, 376}, // L
{10, 10, 12, 1, 10, 13, 385}, // M
{8, 10, 10, 1, 10, 10, 398}, // N
{9, 10, 11, 1, 10, 12, 408}, // O
{8, 10, 10, 1, 10, 10, 420}, // P
{9, 10, 11, 1, 10, 12, 430}, // Q
{8, 10, 10, 1, 10, 10, 442}, // R
{7, 10, 9, 1, 10, 9, 452}, // S
{8, 10, 9, 0, 10, 10, 461}, // T
{8, 10, 10, 1, 10, 10, 471}, // U
{8, 10, 10, 1, 10, 10, 481}, // V
{14, 10, 15, 0, 10, 18, 491}, // W
{10, 10, 11, 0, 10, 13, 509}, // X
{8, 10, 10, 1, 10, 10, 522}, // Y
{8, 10, 9, 0, 10, 10, 532}, // Z
{4, 14, 5, 1, 10, 7, 542}, // [
{7, 10, 7, 0, 10, 9, 549}, // <backslash>
{4, 14, 4, 0, 10, 7, 558}, // ]
{8, 7, 9, 0, 11, 7, 565}, // ^
{8, 2, 9, 0, -2, 2, 572}, // _
{3, 3, 3, 0, 11, 2, 574}, // `
{7, 8, 7, 0, 8, 7, 576}, // a
{7, 10, 9, 1, 10, 9, 583}, // b
{7, 8, 7, 0, 8, 7, 592}, // c
{7, 10, 7, 0, 10, 9, 599}, // d
{8, 8, 9, 0, 8, 8, 608}, // e
{5, 11, 5, 0, 11, 7, 616}, // f
{7, 12, 7, 0, 8, 11, 623}, // g
{6, 10, 7, 1, 10, 8, 634}, // h
{2, 10, 3, 1, 10, 3, 642}, // i
{3, 14, 3, 0, 10, 6, 645}, // j
{6, 10, 7, 1, 10, 8, 651}, // k
{2, 10, 3, 1, 10, 3, 659}, // l
{10, 8, 12, 1, 8, 10, 662}, // m
{6, 8, 7, 1, 8, 6, 672}, // n
{8, 8, 9, 0, 8, 8, 678}, // o
{7, 11, 9, 1, 8, 10, 686}, // p
{7, 11, 7, 0, 8, 10, 696}, // q
{5, 8, 6, 1, 8, 5, 706}, // r
{7, 8, 7, 0, 8, 7, 711}, // s
{5, 10, 5, 0, 10, 7, 718}, // t
{6, 8, 7, 1, 8, 6, 725}, // u
{8, 8, 9, 0, 8, 8, 731}, // v
{10, 8, 11, 0, 8, 10, 739}, // w
{8, 8, 9, 0, 8, 8, 749}, // x
{8, 11, 9, 0, 8, 11, 757}, // y
{7, 8, 7, 0, 8, 7, 768}, // z
{5, 15, 5, 0, 11, 10, 775}, // {
{2, 15, 3, 1, 11, 4, 785}, // |
{6, 15, 6, 0, 11, 12, 789}, // }
{9, 3, 10, 0, 7, 4, 801}, // ~
{2, 12, 4, 2, 8, 3, 805}, // ¡
{7, 13, 7, 0, 10, 12, 808}, // ¢
{8, 10, 9, 0, 10, 10, 820}, // £
{8, 8, 9, 0, 9, 8, 830}, // ¤
{8, 10, 9, 0, 10, 10, 838}, // ¥
{2, 15, 3, 1, 11, 4, 848}, // ¦
{8, 15, 9, 0, 11, 15, 852}, // §
{5, 3, 5, 0, 11, 2, 867}, // ¨
{10, 11, 11, 0, 11, 14, 869}, // ©
{5, 6, 5, 0, 11, 4, 883}, // ª
{7, 7, 9, 1, 8, 7, 887}, // «
{8, 5, 9, 0, 8, 5, 894}, // ¬
{10, 11, 11, 0, 11, 14, 899}, // ®
{8, 2, 9, 0, 12, 2, 913}, // ¯
{4, 5, 5, 1, 11, 3, 915}, // °
{8, 9, 9, 0, 9, 9, 918}, // ±
{5, 8, 5, 0, 11, 5, 927}, // ²
{5, 6, 5, 0, 11, 4, 932}, // ³
{3, 3, 4, 1, 11, 2, 936}, // ´
{6, 12, 9, 2, 8, 9, 938}, // µ
{8, 14, 9, 0, 10, 14, 947}, // ¶
{2, 2, 3, 1, 6, 1, 961}, // ·
{4, 4, 4, 0, 0, 2, 962}, // ¸
{4, 7, 4, 0, 12, 4, 964}, // ¹
{6, 6, 6, 0, 11, 5, 968}, // º
{7, 7, 9, 1, 8, 7, 973}, // »
{12, 12, 13, 0, 12, 18, 980}, // ¼
{12, 11, 13, 0, 11, 17, 998}, // ½
{12, 11, 13, 0, 11, 17, 1015}, // ¾
{7, 12, 9, 1, 8, 11, 1032}, // ¿
{7, 13, 9, 1, 13, 12, 1043}, // À
{7, 13, 9, 1, 13, 12, 1055}, // Á
{7, 13, 9, 1, 13, 12, 1067}, // Â
{7, 13, 9, 1, 13, 12, 1079}, // Ã
{7, 12, 9, 1, 12, 11, 1091}, // Ä
{7, 13, 9, 1, 13, 12, 1102}, // Å
{13, 11, 14, 0, 11, 18, 1114}, // Æ
{9, 14, 11, 1, 10, 16, 1132}, // Ç
{7, 13, 9, 1, 13, 12, 1148}, // È
{7, 13, 9, 1, 13, 12, 1160}, // É
{7, 13, 9, 1, 13, 12, 1172}, // Ê
{7, 12, 9, 1, 12, 11, 1184}, // Ë
{3, 13, 3, 0, 13, 5, 1195}, // Ì
{3, 13, 4, 1, 13, 5, 1200}, // Í
{4, 13, 4, 0, 13, 7, 1205}, // Î
{4, 12, 4, 0, 12, 6, 1212}, // Ï
{9, 10, 10, 0, 10, 12, 1218}, // Ð
{8, 13, 10, 1, 13, 13, 1230}, // Ñ
{9, 13, 11, 1, 13, 15, 1243}, // Ò
{9, 13, 11, 1, 13, 15, 1258}, // Ó
{9, 13, 11, 1, 13, 15, 1273}, // Ô
{9, 13, 11, 1, 13, 15, 1288}, // Õ
{9, 12, 11, 1, 12, 14, 1303}, // Ö
{6, 6, 7, 1, 8, 5, 1317}, // ×
{10, 10, 11, 0, 10, 13, 1322}, // Ø
{8, 13, 10, 1, 13, 13, 1335}, // Ù
{8, 13, 10, 1, 13, 13, 1348}, // Ú
{8, 13, 10, 1, 13, 13, 1361}, // Û
{8, 12, 10, 1, 12, 12, 1374}, // Ü
{8, 13, 10, 1, 13, 13, 1386}, // Ý
{8, 10, 10, 1, 10, 10, 1399}, // Þ
{7, 11, 9, 1, 11, 10, 1409}, // ß
{7, 11, 7, 0, 11, 10, 1419}, // à
{7, 11, 7, 0, 11, 10, 1429}, // á
{7, 11, 7, 0, 11, 10, 1439}, // â
{7, 11, 7, 0, 11, 10, 1449}, // ã
{7, 10, 7, 0, 10, 9, 1459}, // ä
{7, 12, 7, 0, 12, 11, 1468}, // å
{12, 8, 13, 0, 8, 12, 1479}, // æ
{7, 12, 7, 0, 8, 11, 1491}, // ç
{8, 11, 9, 0, 11, 11, 1502}, // è
{8, 11, 9, 0, 11, 11, 1513}, // é
{8, 11, 9, 0, 11, 11, 1524}, // ê
{8, 10, 9, 0, 10, 10, 1535}, // ë
{3, 11, 3, 0, 11, 5, 1545}, // ì
{3, 11, 4, 1, 11, 5, 1550}, // í
{4, 11, 4, 0, 11, 6, 1555}, // î
{4, 10, 4, 0, 10, 5, 1561}, // ï
{8, 11, 9, 0, 11, 11, 1566}, // ð
{6, 11, 7, 1, 11, 9, 1577}, // ñ
{8, 11, 9, 0, 11, 11, 1586}, // ò
{8, 11, 9, 0, 11, 11, 1597}, // ó
{8, 11, 9, 0, 11, 11, 1608}, // ô
{8, 11, 9, 0, 11, 11, 1619}, // õ
{8, 10, 9, 0, 10, 10, 1630}, // ö
{8, 6, 9, 0, 8, 6, 1640}, // ÷
{8, 8, 9, 0, 8, 8, 1646}, // ø
{6, 11, 7, 1, 11, 9, 1654}, // ù
{6, 11, 7, 1, 11, 9, 1663}, // ú
{6, 11, 7, 1, 11, 9, 1672}, // û
{6, 10, 7, 1, 10, 8, 1681}, // ü
{8, 14, 9, 0, 11, 14, 1689}, // ý
{7, 13, 9, 1, 10, 12, 1703}, // þ
{8, 13, 9, 0, 10, 13, 1715}, // ÿ
{7, 12, 9, 1, 12, 11, 1728}, // Ā
{7, 10, 7, 0, 10, 9, 1739}, // ā
{7, 13, 9, 1, 13, 12, 1748}, // Ă
{7, 11, 7, 0, 11, 10, 1760}, // ă
{8, 14, 10, 1, 10, 14, 1770}, // Ą
{8, 12, 9, 0, 8, 12, 1784}, // ą
{9, 13, 11, 1, 13, 15, 1796}, // Ć
{7, 11, 7, 0, 11, 10, 1811}, // ć
{9, 13, 11, 1, 13, 15, 1821}, // Ĉ
{7, 11, 7, 0, 11, 10, 1836}, // ĉ
{9, 12, 11, 1, 12, 14, 1846}, // Ċ
{7, 10, 7, 0, 10, 9, 1860}, // ċ
{9, 13, 11, 1, 13, 15, 1869}, // Č
{7, 11, 7, 0, 11, 10, 1884}, // č
{8, 13, 10, 1, 13, 13, 1894}, // Ď
{9, 11, 10, 0, 11, 13, 1907}, // ď
{9, 10, 10, 0, 10, 12, 1920}, // Đ
{8, 11, 9, 0, 11, 11, 1932}, // đ
{7, 12, 9, 1, 12, 11, 1943}, // Ē
{8, 10, 9, 0, 10, 10, 1954}, // ē
{7, 13, 9, 1, 13, 12, 1964}, // Ĕ
{8, 11, 9, 0, 11, 11, 1976}, // ĕ
{7, 12, 9, 1, 12, 11, 1987}, // Ė
{8, 10, 9, 0, 10, 10, 1998}, // ė
{8, 14, 10, 1, 10, 14, 2008}, // Ę
{8, 12, 9, 0, 8, 12, 2022}, // ę
{7, 13, 9, 1, 13, 12, 2034}, // Ě
{8, 11, 9, 0, 11, 11, 2046}, // ě
{9, 13, 11, 1, 13, 15, 2057}, // Ĝ
{7, 15, 7, 0, 11, 14, 2072}, // ĝ
{9, 13, 11, 1, 13, 15, 2086}, // Ğ
{7, 15, 7, 0, 11, 14, 2101}, // ğ
{9, 12, 11, 1, 12, 14, 2115}, // Ġ
{7, 14, 7, 0, 10, 13, 2129}, // ġ
{9, 14, 11, 1, 10, 16, 2142}, // Ģ
{7, 15, 7, 0, 11, 14, 2158}, // ģ
{8, 13, 10, 1, 13, 13, 2172}, // Ĥ
{6, 13, 7, 1, 13, 10, 2185}, // ĥ
{10, 10, 11, 0, 10, 13, 2195}, // Ħ
{7, 11, 7, 0, 11, 10, 2208}, // ħ
{5, 13, 5, 0, 13, 9, 2218}, // Ĩ
{5, 11, 5, 0, 11, 7, 2227}, // ĩ
{4, 12, 4, 0, 12, 6, 2234}, // Ī
{4, 10, 4, 0, 10, 5, 2240}, // ī
{5, 13, 5, 0, 13, 9, 2245}, // Ĭ
{5, 11, 5, 0, 11, 7, 2254}, // ĭ
{4, 14, 4, 0, 10, 7, 2261}, // Į
{4, 14, 4, 0, 10, 7, 2268}, // į
{2, 12, 3, 1, 12, 3, 2275}, // İ
{2, 8, 3, 1, 8, 2, 2278}, // ı
{8, 10, 10, 1, 10, 10, 2280}, // IJ
{5, 14, 6, 1, 10, 9, 2290}, // ij
{7, 13, 7, 0, 13, 12, 2299}, // Ĵ
{4, 15, 4, 0, 11, 8, 2311}, // ĵ
{8, 14, 10, 1, 10, 14, 2319}, // Ķ
{6, 14, 7, 1, 10, 11, 2333}, // ķ
{6, 8, 7, 1, 8, 6, 2344}, // ĸ
{7, 13, 9, 1, 13, 12, 2350}, // Ĺ
{3, 13, 4, 1, 13, 5, 2362}, // ĺ
{7, 14, 9, 1, 10, 13, 2367}, // Ļ
{4, 14, 4, 0, 10, 7, 2380}, // ļ
{7, 11, 9, 1, 11, 10, 2387}, // Ľ
{4, 11, 5, 1, 11, 6, 2397}, // ľ
{7, 10, 9, 1, 10, 9, 2403}, // Ŀ
{4, 10, 5, 1, 10, 5, 2412}, // ŀ
{8, 10, 9, 0, 10, 10, 2417}, // Ł
{4, 10, 4, 0, 10, 5, 2427}, // ł
{8, 13, 10, 1, 13, 13, 2432}, // Ń
{6, 12, 7, 1, 12, 9, 2445}, // ń
{8, 14, 10, 1, 10, 14, 2454}, // Ņ
{6, 12, 7, 1, 8, 9, 2468}, // ņ
{8, 13, 10, 1, 13, 13, 2477}, // Ň
{6, 11, 7, 1, 11, 9, 2490}, // ň
{7, 11, 7, 0, 11, 10, 2499}, // ʼn
{8, 13, 10, 1, 10, 13, 2509}, // Ŋ
{6, 12, 7, 1, 8, 9, 2522}, // ŋ
{9, 12, 11, 1, 12, 14, 2531}, // Ō
{8, 10, 9, 0, 10, 10, 2545}, // ō
{9, 13, 11, 1, 13, 15, 2555}, // Ŏ
{8, 11, 9, 0, 11, 11, 2570}, // ŏ
{9, 13, 11, 1, 13, 15, 2581}, // Ő
{8, 11, 9, 0, 11, 11, 2596}, // ő
{13, 11, 15, 1, 11, 18, 2607}, // Œ
{13, 8, 14, 0, 8, 13, 2625}, // œ
{8, 13, 10, 1, 13, 13, 2638}, // Ŕ
{5, 11, 6, 1, 11, 7, 2651}, // ŕ
{8, 14, 10, 1, 10, 14, 2658}, // Ŗ
{5, 12, 6, 1, 8, 8, 2672}, // ŗ
{8, 13, 10, 1, 13, 13, 2680}, // Ř
{5, 11, 6, 1, 11, 7, 2693}, // ř
{7, 13, 9, 1, 13, 12, 2700}, // Ś
{7, 11, 7, 0, 11, 10, 2712}, // ś
{7, 13, 9, 1, 13, 12, 2722}, // Ŝ
{7, 11, 7, 0, 11, 10, 2734}, // ŝ
{7, 14, 9, 1, 10, 13, 2744}, // Ş
{7, 12, 7, 0, 8, 11, 2757}, // ş
{7, 13, 9, 1, 13, 12, 2768}, // Š
{7, 11, 7, 0, 11, 10, 2780}, // š
{9, 14, 10, 0, 10, 16, 2790}, // Ţ
{5, 14, 5, 0, 10, 9, 2806}, // ţ
{8, 13, 9, 0, 13, 13, 2815}, // Ť
{6, 11, 6, 0, 11, 9, 2828}, // ť
{8, 10, 9, 0, 10, 10, 2837}, // Ŧ
{5, 10, 5, 0, 10, 7, 2847}, // ŧ
{8, 13, 10, 1, 13, 13, 2854}, // Ũ
{6, 11, 7, 1, 11, 9, 2867}, // ũ
{8, 12, 10, 1, 12, 12, 2876}, // Ū
{6, 10, 7, 1, 10, 8, 2888}, // ū
{8, 13, 10, 1, 13, 13, 2896}, // Ŭ
{6, 11, 7, 1, 11, 9, 2909}, // ŭ
{8, 13, 10, 1, 13, 13, 2918}, // Ů
{6, 11, 7, 1, 11, 9, 2931}, // ů
{8, 13, 10, 1, 13, 13, 2940}, // Ű
{6, 11, 7, 1, 11, 9, 2953}, // ű
{8, 14, 10, 1, 10, 14, 2962}, // Ų
{7, 12, 9, 1, 8, 11, 2976}, // ų
{14, 13, 15, 0, 13, 23, 2987}, // Ŵ
{10, 11, 11, 0, 11, 14, 3010}, // ŵ
{8, 13, 10, 1, 13, 13, 3024}, // Ŷ
{8, 14, 9, 0, 11, 14, 3037}, // ŷ
{8, 12, 10, 1, 12, 12, 3051}, // Ÿ
{8, 13, 9, 0, 13, 13, 3063}, // Ź
{7, 11, 7, 0, 11, 10, 3076}, // ź
{8, 12, 9, 0, 12, 12, 3086}, // Ż
{7, 10, 7, 0, 10, 9, 3098}, // ż
{8, 13, 9, 0, 13, 13, 3107}, // Ž
{7, 11, 7, 0, 11, 10, 3120}, // ž
{8, 10, 9, 0, 10, 10, 3130}, // €
};
static const EpdUnicodeInterval babyblueIntervals[] = {
{0x20, 0x7E, 0x0}, {0xA1, 0xAC, 0x5F}, {0xAE, 0xFF, 0x6B}, {0x100, 0x17E, 0xBD}, {0x20AC, 0x20AC, 0x13C},
};
static const EpdFontData babyblue = {
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4,
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,766 +0,0 @@
/**
* generated by fontconvert.py
* name: ubuntu_10
* size: 10
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t ubuntu_10Bitmaps[7175] = {
0xEE, 0xEE, 0xEE, 0xE6, 0x66, 0x00, 0xEF, 0xE0, 0xEF, 0xDF, 0xBF, 0x6E, 0xC0, 0x1C, 0x61, 0xCE, 0x1C, 0xE1, 0x8E,
0xFF, 0xFF, 0xFF, 0x39, 0xC3, 0x9C, 0x39, 0xCF, 0xFF, 0xFF, 0xF7, 0x18, 0x73, 0x87, 0x38, 0x63, 0x80, 0x0C, 0x03,
0x03, 0xF9, 0xFE, 0xFB, 0xB8, 0x0E, 0x01, 0xE0, 0x7E, 0x07, 0xE0, 0x78, 0x07, 0x01, 0xF9, 0xFF, 0xFB, 0xFC, 0x0C,
0x03, 0x00, 0xC0, 0x7C, 0x38, 0xEE, 0x38, 0xC6, 0x70, 0xC6, 0x70, 0xC6, 0xE0, 0xEE, 0xE0, 0x7D, 0xC0, 0x01, 0x80,
0x03, 0xBE, 0x07, 0x7F, 0x07, 0x63, 0x0E, 0x63, 0x0E, 0x63, 0x1C, 0x77, 0x1C, 0x3E, 0x0F, 0x80, 0x7F, 0x03, 0xDC,
0x0E, 0x30, 0x39, 0xC0, 0xEF, 0x01, 0xF8, 0x07, 0xC0, 0x7F, 0x19, 0xCE, 0xE6, 0x1F, 0x98, 0x3C, 0x79, 0xF1, 0xFF,
0xE3, 0xF9, 0xC0, 0xFF, 0xFE, 0x1C, 0x73, 0x8E, 0x71, 0xC6, 0x38, 0xE3, 0x8E, 0x38, 0xE1, 0x87, 0x1C, 0x38, 0xE1,
0xC7, 0xC3, 0x87, 0x1C, 0x38, 0xE3, 0x86, 0x18, 0x71, 0xC6, 0x18, 0xE3, 0x8E, 0x71, 0xCE, 0x30, 0x0C, 0x03, 0x07,
0xFD, 0xFE, 0x1E, 0x0F, 0xC3, 0xF8, 0xCC, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03,
0x00, 0x77, 0x77, 0x60, 0xFF, 0xF0, 0xFF, 0x80, 0x01, 0xC0, 0x60, 0x38, 0x0E, 0x03, 0x01, 0xC0, 0x70, 0x38, 0x0E,
0x03, 0x81, 0xC0, 0x70, 0x1C, 0x0E, 0x03, 0x80, 0xE0, 0x70, 0x1C, 0x06, 0x03, 0x80, 0x3F, 0x1F, 0xE7, 0xFB, 0x87,
0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x7F, 0x9F, 0xE3, 0xF0, 0x0E, 0x3D, 0xFB, 0xF7, 0xE1,
0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x80, 0x7F, 0x3F, 0xEF, 0x79, 0x0E, 0x03, 0x80, 0xE0, 0x70, 0x3C,
0x1E, 0x0F, 0x07, 0x81, 0xC0, 0xE0, 0x3F, 0xFF, 0xFC, 0x7F, 0x3F, 0xC6, 0x78, 0x0E, 0x03, 0x83, 0xE3, 0xF0, 0xFC,
0x0F, 0x80, 0xE0, 0x1C, 0x07, 0xE7, 0xBF, 0xEF, 0xF0, 0x03, 0xC0, 0xF8, 0x1F, 0x07, 0xE1, 0xFC, 0x7B, 0x8E, 0x73,
0x8E, 0x71, 0xDF, 0xFF, 0xFF, 0x80, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x7F, 0x9F, 0xE7, 0x01, 0xC0, 0x70, 0x1F, 0x87,
0xF0, 0x7E, 0x03, 0xC0, 0x70, 0x1C, 0x0F, 0xE7, 0xBF, 0xEF, 0xF0, 0x0F, 0x87, 0xE3, 0xF1, 0xE0, 0x70, 0x3F, 0xCF,
0xFB, 0xDF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x7B, 0xDF, 0xE3, 0xF0, 0xFF, 0xFF, 0xF0, 0x3C, 0x0E, 0x07, 0x01, 0xC0,
0xE0, 0x38, 0x1C, 0x07, 0x01, 0xC0, 0x60, 0x38, 0x0E, 0x03, 0x80, 0x3F, 0x1F, 0xEF, 0xFB, 0x87, 0xE1, 0xFC, 0xE7,
0xF8, 0xFC, 0x7F, 0xB8, 0xFE, 0x1F, 0x87, 0xF7, 0xFF, 0xE7, 0xF0, 0x3F, 0x1F, 0xEF, 0x7B, 0x87, 0xE1, 0xF8, 0x7F,
0x7F, 0xFF, 0x7F, 0xC0, 0xF0, 0x38, 0x1E, 0x3F, 0x3F, 0x87, 0xC0, 0xFF, 0x80, 0x00, 0xFF, 0x80, 0x77, 0x70, 0x00,
0x00, 0x07, 0x77, 0x76, 0x01, 0xC3, 0xF7, 0xFB, 0xF0, 0xF0, 0x3F, 0x07, 0xF8, 0x3F, 0x03, 0xC0, 0xFF, 0xFF, 0xF0,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0xE0, 0x3F, 0x07, 0xF8, 0x3F, 0x03, 0xC7, 0xF7, 0xFB, 0xF0, 0xE0, 0x00, 0xFE,
0xFF, 0xEF, 0x07, 0x07, 0x0F, 0x1E, 0x1C, 0x38, 0x38, 0x00, 0x00, 0x38, 0x38, 0x38, 0x07, 0xF8, 0x07, 0xFF, 0x83,
0xFB, 0xF0, 0xF0, 0x1E, 0x70, 0x03, 0x9C, 0xFE, 0x6E, 0x7F, 0x9F, 0x9E, 0xE7, 0xEF, 0x39, 0xFB, 0x8E, 0x7E, 0xE3,
0x9F, 0xB8, 0xE7, 0xE7, 0xBB, 0xB9, 0xFF, 0xE7, 0x3F, 0xF1, 0xC0, 0x00, 0x3C, 0x00, 0x0F, 0xE6, 0x01, 0xFF, 0x80,
0x1F, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x73, 0x81, 0xCE, 0x06, 0x38, 0x38, 0x70, 0xFF,
0xC7, 0xFF, 0x9C, 0x0E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xC0, 0xFF, 0x8F, 0xFC, 0xE3, 0xEE, 0x0E, 0xE0, 0xEE, 0x3E,
0xFF, 0xCF, 0xFE, 0xE3, 0xEE, 0x07, 0xE0, 0x7E, 0x0F, 0xE3, 0xEF, 0xFE, 0xFF, 0xC0, 0x0F, 0xE3, 0xFF, 0x7E, 0xE7,
0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0xFF,
0x87, 0xFF, 0x38, 0xFD, 0xC1, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x1F, 0xC1, 0xEE,
0x3F, 0x7F, 0xF3, 0xFE, 0x00, 0xFF, 0xDF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xE7, 0xFC, 0xE0, 0x1C, 0x03,
0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xE0, 0x38, 0x0F, 0xFB, 0xFE, 0xE0, 0x38,
0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x00, 0x0F, 0xF3, 0xFF, 0x7E, 0xF7, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00,
0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E,
0x03, 0xFF, 0xFF, 0xFF, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x30, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF8, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x7E, 0xFF, 0xFB, 0xF8,
0xE0, 0xF7, 0x0F, 0x38, 0xF1, 0xC7, 0x0E, 0x70, 0x77, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x39, 0xE1, 0xC7,
0x8E, 0x1E, 0x70, 0x7B, 0x81, 0xC0, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E,
0x03, 0x80, 0xE0, 0x3F, 0xFF, 0xFC, 0x70, 0x0E, 0x78, 0x0E, 0x78, 0x1E, 0x78, 0x1F, 0x6C, 0x3F, 0x6C, 0x37, 0xEE,
0x37, 0xE6, 0x67, 0xE7, 0x67, 0xE3, 0xE7, 0xE3, 0xC7, 0xE3, 0xC7, 0xE1, 0xC7, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x3F,
0x81, 0xFE, 0x0F, 0xF8, 0x7F, 0xE3, 0xF7, 0x1F, 0x9C, 0xFC, 0xF7, 0xE3, 0xBF, 0x0F, 0xF8, 0x7F, 0xC1, 0xFE, 0x07,
0xF0, 0x3F, 0x80, 0xE0, 0x1F, 0xE0, 0x7F, 0xE1, 0xFB, 0xE3, 0x81, 0xEF, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0F,
0xE0, 0x1D, 0xC0, 0x3B, 0xC0, 0x73, 0x81, 0xE7, 0xEF, 0x87, 0xFE, 0x07, 0xF8, 0x00, 0xFF, 0x8F, 0xFC, 0xE3, 0xEE,
0x0E, 0xE0, 0xEE, 0x0E, 0xE7, 0xEF, 0xFC, 0xFF, 0x8E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x00, 0x1F,
0xE0, 0x7F, 0xE1, 0xFB, 0xE3, 0x81, 0xEF, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0F, 0xE0, 0x1D, 0xC0, 0x3B, 0xC0,
0x73, 0x81, 0xE7, 0xEF, 0x87, 0xFE, 0x07, 0xF8, 0x01, 0xE0, 0x01, 0xF8, 0x01, 0xF0, 0x01, 0xE0, 0xFF, 0x8F, 0xFC,
0xE3, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE3, 0xEF, 0xFC, 0xFF, 0x8E, 0x38, 0xE3, 0xCE, 0x1C, 0xE0, 0xEE, 0x0F, 0xE0,
0x70, 0x3F, 0xCF, 0xF9, 0xE7, 0x38, 0x07, 0x00, 0xF0, 0x1F, 0x81, 0xFC, 0x0F, 0xC0, 0x3C, 0x03, 0x80, 0x77, 0x3F,
0xFF, 0x9F, 0xE0, 0xFF, 0xFF, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7D, 0xE3, 0xFE, 0x1F, 0xC0, 0xE0, 0x1F, 0x80, 0xF7, 0x03, 0x9C, 0x0E, 0x70,
0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x38, 0x1C, 0xE0, 0x73, 0x80, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x1E, 0x00, 0x78, 0x00,
0xE0, 0x00, 0xFC, 0x00, 0x1D, 0xC3, 0x83, 0xB8, 0x78, 0x77, 0x0F, 0x0E, 0xE3, 0xE3, 0x9C, 0x7E, 0x71, 0xCD, 0xCE,
0x3B, 0xB9, 0xC7, 0x73, 0xB0, 0xEC, 0x7E, 0x0F, 0x87, 0xC1, 0xF0, 0xF8, 0x3C, 0x1E, 0x03, 0x81, 0xC0, 0xF0, 0x7B,
0xC3, 0x8E, 0x38, 0x7B, 0xC1, 0xFC, 0x07, 0xC0, 0x3E, 0x00, 0xE0, 0x0F, 0x80, 0xFE, 0x07, 0x78, 0x71, 0xC7, 0x87,
0x38, 0x3B, 0xC0, 0xE0, 0xE0, 0x7F, 0x83, 0x9C, 0x3C, 0x71, 0xC3, 0x9C, 0x0E, 0xE0, 0x7E, 0x01, 0xF0, 0x0F, 0x00,
0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0x7F, 0xE7, 0xFE, 0x00, 0xE0, 0x1C, 0x03, 0xC0, 0x78,
0x07, 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x70, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xF1, 0x8C, 0x63, 0x18,
0xC6, 0x31, 0x8C, 0x63, 0x18, 0xC6, 0x3F, 0xF0, 0xE0, 0x18, 0x07, 0x01, 0xC0, 0x38, 0x0E, 0x03, 0x80, 0x70, 0x1C,
0x07, 0x00, 0xE0, 0x38, 0x0E, 0x01, 0xC0, 0x70, 0x0C, 0x03, 0x80, 0xE0, 0x18, 0x07, 0xFF, 0xC6, 0x31, 0x8C, 0x63,
0x18, 0xC6, 0x31, 0x8C, 0x63, 0x18, 0xFF, 0xF0, 0x0F, 0x01, 0xE0, 0x7E, 0x0F, 0xC3, 0x9C, 0xE3, 0x9C, 0x3B, 0x07,
0xFF, 0xFF, 0xFF, 0xE7, 0x9C, 0x60, 0x3F, 0x0F, 0xE3, 0x3C, 0x07, 0x3F, 0xDF, 0xF7, 0x9F, 0x87, 0x79, 0xDF, 0xF3,
0xFC, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0x3F, 0xF7, 0xDE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0, 0x7E, 0x1D,
0xEF, 0xBF, 0xE7, 0xF8, 0x3F, 0x3F, 0xBE, 0xDC, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xFB, 0x3F, 0xCF, 0xE0, 0x01, 0xC0,
0x70, 0x1C, 0x07, 0x01, 0xCF, 0xF7, 0xFF, 0xEF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFE, 0x77, 0xFC, 0xFF, 0x3F,
0x1F, 0xEF, 0xFF, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xF9, 0x9F, 0xE3, 0xF8, 0x3F, 0x7F, 0xFA, 0xE0, 0xE0, 0xFE,
0xFE, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x3F, 0xDF, 0xFF, 0x9F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F,
0x87, 0xFF, 0xDF, 0xF3, 0xFC, 0x07, 0x77, 0xFF, 0xEF, 0xF0, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x3F, 0xCF, 0xFB,
0xDF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xFC, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x1C, 0x70,
0x00, 0x00, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0xFF, 0xFE, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0,
0x38, 0xEE, 0x7B, 0xBC, 0xFE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39, 0xCE, 0x3B, 0x8F, 0xE7, 0x39, 0xCE, 0x73, 0x9C,
0xE7, 0x39, 0xCE, 0x7B, 0xEE, 0xFF, 0xFE, 0xFF, 0xFE, 0xF7, 0xEF, 0xE3, 0x87, 0xE1, 0x87, 0xE1, 0xC7, 0xE1, 0xC7,
0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xE1, 0xC7, 0xFF, 0x3F, 0xEF, 0x7F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1,
0xF8, 0x7E, 0x1C, 0x3F, 0x0F, 0xF3, 0xFF, 0x70, 0xEE, 0x0F, 0xC1, 0xF8, 0x3F, 0x0E, 0xFB, 0xCF, 0xF0, 0xFC, 0x00,
0xFF, 0x1F, 0xF3, 0xDF, 0x70, 0xEE, 0x0F, 0xC1, 0xF8, 0x3F, 0x0E, 0xFB, 0xDF, 0xFB, 0xFE, 0x70, 0x0E, 0x01, 0xC0,
0x38, 0x00, 0x3F, 0xDF, 0xFF, 0x9F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xFB, 0xDF, 0xF3, 0xFC, 0x07, 0x01, 0xC0,
0x70, 0x1C, 0xFF, 0xFF, 0xDF, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x80, 0x3F, 0x3F, 0x9E, 0xCE, 0x07, 0xC1, 0xF8,
0x3E, 0x07, 0x67, 0xBF, 0xDF, 0xC0, 0xE0, 0xE0, 0xE0, 0xFE, 0xFE, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF6, 0x7E,
0x7E, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xFB, 0xDF, 0xF3, 0xFC, 0xE0, 0xFC, 0x39, 0xC7,
0x38, 0xE7, 0x38, 0x77, 0x0E, 0xE1, 0xF8, 0x1F, 0x03, 0xC0, 0x38, 0x00, 0xE1, 0xC7, 0xE1, 0xC7, 0x73, 0xC7, 0x73,
0xC6, 0x73, 0xEE, 0x37, 0xEE, 0x3F, 0x6E, 0x3E, 0x7C, 0x1E, 0x7C, 0x1E, 0x38, 0x1C, 0x38, 0xF1, 0xCE, 0x78, 0xEE,
0x1F, 0x81, 0xE0, 0x1C, 0x07, 0xC1, 0xFC, 0x7B, 0x8E, 0x3B, 0x87, 0x80, 0xE1, 0xDC, 0x39, 0xC7, 0x38, 0xE7, 0x38,
0x77, 0x0E, 0xE0, 0xF8, 0x1F, 0x03, 0xE0, 0x38, 0x07, 0x0B, 0xC1, 0xF8, 0x3E, 0x00, 0x7F, 0x9F, 0xE0, 0x78, 0x3C,
0x0E, 0x07, 0x03, 0xC1, 0xE0, 0x70, 0x3F, 0xFF, 0xFC, 0x1E, 0x3C, 0xF1, 0xC3, 0x87, 0x0E, 0x1C, 0x79, 0xE3, 0xE3,
0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x78, 0x78, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xE1, 0xE1, 0xC3, 0x87, 0x0E,
0x1C, 0x3C, 0x3C, 0xF9, 0xE3, 0x87, 0x0E, 0x1C, 0x38, 0xF3, 0xC7, 0x80, 0x3C, 0x6F, 0xFD, 0xFF, 0xB1, 0xE0, 0xEF,
0xE0, 0x06, 0x66, 0xEE, 0xEE, 0xEE, 0xE0, 0x0E, 0x03, 0x80, 0xE0, 0x7E, 0x3F, 0x9F, 0x67, 0x03, 0x80, 0xE0, 0x38,
0x07, 0x01, 0xF6, 0x3F, 0x87, 0xE0, 0xE0, 0x38, 0x0E, 0x00, 0x1F, 0xC7, 0xF3, 0xD8, 0xE0, 0x38, 0x0E, 0x03, 0x83,
0xFE, 0xFF, 0x8E, 0x03, 0x80, 0xC0, 0x30, 0x1F, 0xF7, 0xFC, 0x60, 0x6F, 0xFC, 0xFF, 0x1F, 0xE7, 0x0C, 0xE1, 0x8E,
0xF1, 0xFE, 0x7F, 0xEC, 0x0C, 0xE0, 0x77, 0x0E, 0x70, 0xE3, 0x9C, 0x3F, 0x81, 0xF8, 0x0F, 0x07, 0xFE, 0x7F, 0xE0,
0x60, 0x06, 0x07, 0xFE, 0x7F, 0xE0, 0x60, 0x06, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x3F, 0x8F, 0xE7, 0xB9, 0xC0,
0x78, 0x0F, 0xC7, 0xF9, 0xCF, 0x61, 0xD8, 0x77, 0x9C, 0xFE, 0x1F, 0x80, 0xE0, 0x19, 0xDE, 0x7F, 0x9F, 0xC0, 0xEE,
0xEE, 0x0F, 0xE0, 0x7F, 0xF1, 0xFB, 0xF3, 0x80, 0xEE, 0x7E, 0xFD, 0xD9, 0xF7, 0x01, 0xEE, 0x03, 0xDC, 0x07, 0xDE,
0xDF, 0x9F, 0xBB, 0x80, 0xE7, 0xEF, 0xC7, 0xFF, 0x03, 0xF8, 0x00, 0x7E, 0x2E, 0x06, 0x7F, 0x77, 0xE7, 0x77, 0x7E,
0x1D, 0xCE, 0x73, 0xB9, 0xDC, 0xF7, 0x1D, 0xC3, 0xB8, 0xE7, 0x1D, 0xC0, 0xFF, 0xFF, 0xF0, 0x0C, 0x03, 0x00, 0xC0,
0x30, 0x0C, 0x03, 0xFF, 0xF0, 0x0F, 0xE0, 0x7F, 0xF1, 0xFB, 0xF3, 0x80, 0xEE, 0xFC, 0xFD, 0xFD, 0xF3, 0xB9, 0xE7,
0xE3, 0xCF, 0xC7, 0xDD, 0xDF, 0xB9, 0xBB, 0x80, 0xE7, 0xEF, 0xC7, 0xFF, 0x03, 0xF8, 0x00, 0x7E, 0xFC, 0x7D, 0xFF,
0xBF, 0x7F, 0xEF, 0x80, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0xFF, 0xFF, 0xF0, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0x03, 0xFF,
0xFF, 0xC0, 0x7D, 0xDC, 0x38, 0xE3, 0xCF, 0x1C, 0x3F, 0x7C, 0xF8, 0x71, 0xE1, 0xE1, 0xD3, 0xFE, 0x3B, 0xDC, 0xC0,
0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xF9, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x38, 0x0E, 0x00,
0x1F, 0xF3, 0xF7, 0x7F, 0x77, 0xF7, 0xFF, 0x77, 0xF7, 0x7F, 0x77, 0xF7, 0x3F, 0x70, 0xF7, 0x07, 0x70, 0x77, 0x07,
0x70, 0x77, 0x07, 0x70, 0x77, 0x07, 0x70, 0x77, 0x07, 0x70, 0xFF, 0x80, 0x33, 0x8F, 0xE0, 0x3F, 0xF3, 0x33, 0x33,
0x3E, 0x3F, 0x98, 0xEC, 0x76, 0x3B, 0x1D, 0xDC, 0x7C, 0x67, 0x1D, 0xC7, 0xB8, 0xEF, 0x1D, 0xCE, 0xF7, 0xB9, 0xDC,
0x67, 0x00, 0x30, 0x18, 0x78, 0x1C, 0x3C, 0x1C, 0x06, 0x0E, 0x03, 0x0E, 0x01, 0x87, 0x00, 0xC7, 0x00, 0x63, 0x8E,
0x03, 0x8F, 0x01, 0x87, 0x81, 0xC7, 0xC0, 0xC7, 0x60, 0xE3, 0xF8, 0xE0, 0x18, 0x70, 0x0C, 0x30, 0x38, 0x78, 0x18,
0x3C, 0x1C, 0x06, 0x1C, 0x03, 0x0E, 0x01, 0x8E, 0x00, 0xC7, 0x00, 0x67, 0x7E, 0x03, 0xBB, 0x03, 0x81, 0x81, 0x81,
0xC1, 0xC1, 0xC0, 0xC1, 0xE0, 0xE0, 0xC0, 0xE0, 0xFE, 0x7C, 0x0E, 0x1F, 0x07, 0x01, 0xC1, 0xC0, 0xF0, 0xE0, 0x1E,
0x38, 0x03, 0x9C, 0x04, 0xE6, 0x03, 0xF3, 0x8E, 0x01, 0xC7, 0x80, 0x71, 0xE0, 0x38, 0xF8, 0x0E, 0x76, 0x07, 0x1F,
0xC1, 0xC0, 0x60, 0xE0, 0x18, 0x1C, 0x0F, 0x07, 0x00, 0x00, 0x00, 0x60, 0x70, 0x38, 0x38, 0x38, 0x38, 0x1C, 0x0F,
0x73, 0xF9, 0xFC, 0x0E, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, 0x00, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0xC0,
0x3F, 0x01, 0xCE, 0x07, 0x38, 0x18, 0xE0, 0xE1, 0xC3, 0xFF, 0x1F, 0xFE, 0x70, 0x39, 0xC0, 0xEE, 0x01, 0xF8, 0x07,
0x01, 0xC0, 0x0F, 0x00, 0x78, 0x00, 0xC0, 0x00, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0xC0, 0x3F, 0x01, 0xCE,
0x07, 0x38, 0x18, 0xE0, 0xE1, 0xC3, 0xFF, 0x1F, 0xFE, 0x70, 0x39, 0xC0, 0xEE, 0x01, 0xF8, 0x07, 0x03, 0x00, 0x1E,
0x00, 0xFC, 0x03, 0x30, 0x00, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x0F, 0xC0, 0x3F, 0x01, 0xCE, 0x07, 0x38, 0x18,
0xE0, 0xE1, 0xC3, 0xFF, 0x1F, 0xFE, 0x70, 0x39, 0xC0, 0xEE, 0x01, 0xF8, 0x07, 0x0F, 0xE0, 0x7F, 0x81, 0xFC, 0x00,
0x00, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x73, 0x81, 0xCE, 0x06, 0x38, 0x38, 0x70, 0xFF, 0xC7,
0xFF, 0x9C, 0x0E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xC0, 0x1D, 0xC0, 0x77, 0x00, 0x00, 0x01, 0xE0, 0x07, 0x80, 0x1E,
0x00, 0xFC, 0x03, 0xF0, 0x1C, 0xE0, 0x73, 0x81, 0x8E, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xE7, 0x03, 0x9C, 0x0E, 0xE0,
0x1F, 0x80, 0x70, 0x07, 0x80, 0x3E, 0x00, 0xF8, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0xF0, 0x1C, 0xE0,
0x73, 0x81, 0x8E, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF, 0xE7, 0x03, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x70, 0x00, 0xFF, 0xE0,
0x1F, 0xFC, 0x07, 0xC0, 0x01, 0xD8, 0x00, 0x3B, 0x00, 0x0E, 0x60, 0x03, 0x8F, 0xF0, 0x71, 0xFE, 0x1C, 0x30, 0x03,
0xFE, 0x00, 0xFF, 0xC0, 0x1C, 0x18, 0x07, 0x03, 0x01, 0xE0, 0x7F, 0xF8, 0x0F, 0xF8, 0x0F, 0xE3, 0xFF, 0x7E, 0xE7,
0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0x70,
0x0F, 0x80, 0xF0, 0x1C, 0x03, 0x80, 0x38, 0x03, 0x00, 0x01, 0xFF, 0xBF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F,
0xCF, 0xF9, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x03, 0x00, 0xE0, 0x38, 0x06, 0x00, 0x01,
0xFF, 0xBF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xCF, 0xF9, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF,
0xFF, 0xF0, 0x0C, 0x03, 0xC0, 0xFC, 0x19, 0x80, 0x01, 0xFF, 0xBF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xCF,
0xF9, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xF0, 0x3B, 0x87, 0x70, 0x00, 0x7F, 0xEF, 0xFD, 0xC0,
0x38, 0x07, 0x00, 0xE0, 0x1F, 0xF3, 0xFE, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xFF, 0xFF, 0xFC, 0xE7, 0x9C,
0x60, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE0, 0x33, 0xFD, 0xC0, 0x73, 0x9C, 0xE7, 0x39, 0xCE,
0x73, 0x9C, 0xE7, 0x39, 0xC0, 0x18, 0x79, 0xFB, 0x30, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C,
0x38, 0x70, 0xE1, 0xC0, 0xEE, 0xEE, 0x00, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
0x38, 0x38, 0x38, 0x3F, 0xC0, 0x7F, 0xE0, 0xE7, 0xE1, 0xC1, 0xE3, 0x81, 0xC7, 0x03, 0xBF, 0x87, 0x7F, 0x0E, 0x38,
0x1C, 0x70, 0x38, 0xE0, 0x71, 0xC1, 0xE3, 0x9F, 0x87, 0xFE, 0x0F, 0xF0, 0x00, 0x0F, 0xC0, 0xFE, 0x07, 0xF0, 0x00,
0x0E, 0x03, 0xF8, 0x1F, 0xE0, 0xFF, 0x87, 0xFE, 0x3F, 0x71, 0xF9, 0xCF, 0xCF, 0x7E, 0x3B, 0xF0, 0xFF, 0x87, 0xFC,
0x1F, 0xE0, 0x7F, 0x03, 0xF8, 0x0E, 0x06, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xFC,
0x3F, 0x7C, 0x70, 0x3D, 0xE0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 0x01, 0xFC, 0x03, 0xB8, 0x07, 0x78, 0x0E, 0x70, 0x3C,
0xFD, 0xF0, 0xFF, 0xC0, 0xFF, 0x00, 0x01, 0xC0, 0x07, 0x80, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xFC,
0x3F, 0x7C, 0x70, 0x3D, 0xE0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 0x01, 0xFC, 0x03, 0xB8, 0x07, 0x78, 0x0E, 0x70, 0x3C,
0xFD, 0xF0, 0xFF, 0xC0, 0xFF, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x66, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0xFC,
0x3F, 0x7C, 0x70, 0x3D, 0xE0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 0x01, 0xFC, 0x03, 0xB8, 0x07, 0x78, 0x0E, 0x70, 0x3C,
0xFD, 0xF0, 0xFF, 0xC0, 0xFF, 0x00, 0x0F, 0x60, 0x1F, 0xC0, 0x7F, 0x00, 0x00, 0x01, 0xFE, 0x07, 0xFE, 0x1F, 0xBE,
0x38, 0x1E, 0xF0, 0x1D, 0xC0, 0x3B, 0x80, 0x77, 0x00, 0xFE, 0x01, 0xDC, 0x03, 0xBC, 0x07, 0x38, 0x1E, 0x7E, 0xF8,
0x7F, 0xE0, 0x7F, 0x80, 0x0E, 0xE0, 0x1D, 0xC0, 0x00, 0x00, 0xFF, 0x03, 0xFF, 0x0F, 0xDF, 0x1C, 0x0F, 0x78, 0x0E,
0xE0, 0x1D, 0xC0, 0x3B, 0x80, 0x7F, 0x00, 0xEE, 0x01, 0xDE, 0x03, 0x9C, 0x0F, 0x3F, 0x7C, 0x3F, 0xF0, 0x3F, 0xC0,
0x61, 0xBC, 0xE7, 0xF8, 0xFC, 0x1E, 0x0F, 0xC7, 0xFB, 0xCE, 0x61, 0x80, 0x1F, 0xFC, 0x7F, 0xF9, 0xFB, 0xE3, 0x83,
0xEE, 0x0F, 0xDC, 0x3F, 0xB8, 0xF7, 0x71, 0xCF, 0xE7, 0x1D, 0xDC, 0x3B, 0xF0, 0x73, 0xE1, 0xE7, 0xEF, 0x8F, 0xFE,
0x1F, 0xF8, 0x00, 0x0C, 0x00, 0xE0, 0x07, 0x00, 0x30, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC, 0x03, 0x80, 0x38, 0x07, 0x00,
0x60, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC, 0x06, 0x00, 0xF0, 0x1F, 0x81, 0x98, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC, 0x1D, 0xC1,
0xDC, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC, 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0C, 0x00, 0x00, 0x70, 0x3F, 0xC1, 0xCE, 0x1E,
0x38, 0xE1, 0xCE, 0x07, 0x70, 0x3F, 0x00, 0xF8, 0x07, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E,
0x00, 0xE0, 0x0E, 0x00, 0xFF, 0x8F, 0xFC, 0xE7, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEE, 0x7E, 0xFF, 0xCF, 0xF8,
0xE0, 0x0E, 0x00, 0xE0, 0x00, 0x3F, 0x87, 0xF8, 0x7B, 0xCE, 0x1C, 0xE1, 0xCE, 0x38, 0xE7, 0x0E, 0x60, 0xEF, 0x8E,
0x7C, 0xE7, 0xEE, 0x1F, 0xE0, 0x7E, 0xEF, 0xEF, 0xEE, 0xFC, 0x18, 0x07, 0x00, 0xE0, 0x18, 0x00, 0x0F, 0xC3, 0xF8,
0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7, 0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x03, 0x01, 0xC0, 0xE0, 0x30, 0x00, 0x0F, 0xC3,
0xF8, 0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7, 0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x0E, 0x03, 0xC1, 0xF0, 0xEE, 0x00, 0x0F,
0xC3, 0xF8, 0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7, 0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x1F, 0x8F, 0xE3, 0xF0, 0x00, 0x00,
0x0F, 0xC3, 0xF8, 0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7, 0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x3B, 0x8E, 0xE0, 0x00, 0x00,
0x3F, 0x0F, 0xE3, 0x3C, 0x07, 0x3F, 0xDF, 0xF7, 0x9F, 0x87, 0x79, 0xDF, 0xF3, 0xFC, 0x1F, 0x06, 0xC1, 0xB0, 0x7C,
0x00, 0x0F, 0xC3, 0xF8, 0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7, 0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x3F, 0x7E, 0x1F, 0xFF,
0x8C, 0xFD, 0xE0, 0x38, 0x73, 0xFF, 0xFB, 0xFF, 0xFD, 0xE7, 0x00, 0xC3, 0x80, 0x7B, 0xFB, 0x3F, 0xFF, 0x8F, 0xFF,
0xC0, 0x3F, 0x3F, 0xBE, 0xDC, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xFB, 0x3F, 0xCF, 0xE1, 0x80, 0xE0, 0xF0, 0xF8, 0x38,
0x0F, 0x01, 0xE0, 0x38, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3E, 0x67, 0xF8, 0xFE,
0x07, 0x03, 0xC1, 0xE0, 0x70, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3E, 0x67, 0xF8,
0xFE, 0x0C, 0x07, 0x83, 0xF0, 0xCC, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3E, 0x67,
0xF8, 0xFE, 0x7F, 0x9F, 0xE0, 0x00, 0x00, 0x3F, 0x1F, 0xEF, 0xFF, 0x87, 0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xF9, 0x9F,
0xE3, 0xF8, 0xC7, 0x1C, 0x60, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x33, 0xB9, 0x80, 0x73, 0x9C, 0xE7, 0x39,
0xCE, 0x73, 0x9C, 0x18, 0x79, 0xFB, 0x30, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0xEF, 0xDC,
0x00, 0x03, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x00, 0x07, 0xE1, 0xFC, 0x3E, 0x07, 0xE0, 0x1C,
0x7F, 0x9F, 0xF7, 0xDF, 0xE0, 0xFC, 0x1F, 0x83, 0x70, 0xEE, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8, 0x3F, 0x8F, 0xE7, 0xF0,
0x00, 0x00, 0x3F, 0xCF, 0xFB, 0xDF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x18, 0x03, 0x80,
0x38, 0x07, 0x00, 0x00, 0x7E, 0x1F, 0xE7, 0xFE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8,
0x07, 0x00, 0xE0, 0x38, 0x06, 0x00, 0x00, 0x7E, 0x1F, 0xE7, 0xFE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7,
0x9F, 0xE1, 0xF8, 0x0C, 0x03, 0xC0, 0xFC, 0x19, 0x80, 0x00, 0x7E, 0x1F, 0xE7, 0xFE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0,
0x7E, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8, 0x3D, 0x87, 0xF1, 0xFC, 0x00, 0x00, 0x00, 0x7E, 0x1F, 0xE7, 0xFE, 0xE1, 0xDC,
0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8, 0x3B, 0x87, 0x70, 0x00, 0x00, 0x03, 0xF0, 0xFF, 0x3F, 0xF7,
0x0E, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0xEF, 0xBC, 0xFF, 0x0F, 0xC0, 0x1C, 0x07, 0x81, 0xC0, 0x00, 0x00, 0x3F, 0xFF,
0xFC, 0x00, 0x00, 0x07, 0x01, 0xE0, 0x70, 0x3F, 0xCF, 0xFB, 0xEF, 0x73, 0xEE, 0x7F, 0xDD, 0xFF, 0x3F, 0xCE, 0xFB,
0xDF, 0xF3, 0xFC, 0x00, 0x38, 0x0F, 0x01, 0xE0, 0x38, 0x00, 0x38, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87,
0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0x07, 0x03, 0xC1, 0xE0, 0x70, 0x00, 0x38, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F,
0x87, 0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0x0C, 0x07, 0x83, 0xF0, 0xCC, 0x00, 0x38, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E,
0x1F, 0x87, 0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0x77, 0x1D, 0xC0, 0x00, 0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8,
0x7E, 0x1F, 0x87, 0xFB, 0xDF, 0xF3, 0xFC, 0x03, 0x00, 0xE0, 0x38, 0x06, 0x00, 0x01, 0xC3, 0xB8, 0x73, 0x8E, 0x71,
0xCE, 0x70, 0xEE, 0x1D, 0xC1, 0xF0, 0x3E, 0x07, 0xC0, 0x70, 0x0E, 0x17, 0x83, 0xF0, 0x7C, 0x00, 0xE0, 0x1C, 0x03,
0x80, 0x70, 0x0E, 0x01, 0xFE, 0x3F, 0xE7, 0xBE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7, 0xBF, 0xF7, 0xFC,
0xE0, 0x1C, 0x03, 0x80, 0x70, 0x00, 0x3B, 0x87, 0x70, 0x00, 0x00, 0x0E, 0x1D, 0xC3, 0x9C, 0x73, 0x8E, 0x73, 0x87,
0x70, 0xEE, 0x0F, 0x81, 0xF0, 0x3E, 0x03, 0x80, 0x70, 0xBC, 0x1F, 0x83, 0xE0, 0x00, 0x0F, 0xC0, 0x3F, 0x00, 0x00,
0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0xFC, 0x03, 0xF0, 0x1C, 0xE0, 0x73, 0x81, 0x8E, 0x0E, 0x1C, 0x3F, 0xF1, 0xFF,
0xE7, 0x03, 0x9C, 0x0E, 0xE0, 0x1F, 0x80, 0x70, 0x3F, 0x8F, 0xE0, 0x00, 0x00, 0x3F, 0x0F, 0xE3, 0x3C, 0x07, 0x3F,
0xDF, 0xF7, 0x9F, 0x87, 0x79, 0xDF, 0xF3, 0xFC, 0x0F, 0xC0, 0x3F, 0x00, 0x78, 0x00, 0x00, 0x07, 0x80, 0x1E, 0x00,
0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x73, 0x81, 0xCE, 0x06, 0x38, 0x38, 0x70, 0xFF, 0xC7, 0xFF, 0x9C, 0x0E, 0x70, 0x3B,
0x80, 0x7E, 0x01, 0xC0, 0x3B, 0x87, 0xC1, 0xF0, 0x00, 0x00, 0x0F, 0xC3, 0xF8, 0xCF, 0x01, 0xCF, 0xF7, 0xFD, 0xE7,
0xE1, 0xDE, 0x77, 0xFC, 0xFF, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x03, 0xF0, 0x0F, 0xC0, 0x73, 0x81, 0xCE, 0x06, 0x38,
0x38, 0x70, 0xFF, 0xC7, 0xFF, 0x9C, 0x0E, 0x70, 0x3B, 0x80, 0x7E, 0x01, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, 0x03,
0xC0, 0x3F, 0x0F, 0xE3, 0x3C, 0x07, 0x3F, 0xDF, 0xF7, 0x9F, 0x87, 0x79, 0xDF, 0xF3, 0xFC, 0x0E, 0x07, 0x00, 0xF0,
0x01, 0x80, 0x38, 0x07, 0x00, 0x60, 0x00, 0x00, 0xFE, 0x3F, 0xF7, 0xEE, 0x70, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0,
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0xEF, 0x3F, 0xF1, 0xFF, 0x07, 0x07, 0x87, 0x83, 0x80, 0x01, 0xF9, 0xFD,
0xF6, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0xD9, 0xFE, 0x7F, 0x03, 0x00, 0x78, 0x0F, 0xC0, 0xCC, 0x00, 0x00, 0xFE,
0x3F, 0xF7, 0xEE, 0x70, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0xEF, 0x3F,
0xF1, 0xFF, 0x0C, 0x0F, 0x0F, 0xC6, 0x60, 0x01, 0xF9, 0xFD, 0xF6, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0xD9, 0xFE,
0x7F, 0x07, 0x00, 0x70, 0x07, 0x00, 0x00, 0x0F, 0xE3, 0xFF, 0x7E, 0xE7, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00,
0xE0, 0x0E, 0x00, 0xE0, 0x07, 0x00, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0x0C, 0x0F, 0x03, 0x00, 0x00, 0x01, 0xF9, 0xFD,
0xF6, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0xD9, 0xFE, 0x7F, 0x0C, 0xC0, 0xFC, 0x07, 0x80, 0x30, 0x00, 0x00, 0xFE,
0x3F, 0xF7, 0xEE, 0x70, 0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0xEF, 0x3F,
0xF1, 0xFF, 0x33, 0x1F, 0x87, 0x81, 0x80, 0x01, 0xF9, 0xFD, 0xF6, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0xD9, 0xFE,
0x7F, 0x19, 0x80, 0xFC, 0x03, 0xC0, 0x0C, 0x00, 0x00, 0x7F, 0xC3, 0xFF, 0x9C, 0x7E, 0xE0, 0xF7, 0x03, 0xF8, 0x0F,
0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0xE0, 0xF7, 0x1F, 0xBF, 0xF9, 0xFF, 0x00, 0x01, 0xD8, 0x07, 0x60,
0x1D, 0x80, 0x76, 0x01, 0xD8, 0xFF, 0x07, 0xFC, 0x3E, 0xF0, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3,
0xE7, 0x07, 0xFC, 0x0F, 0xF0, 0x3F, 0xC0, 0x7F, 0xE0, 0xE7, 0xE1, 0xC1, 0xE3, 0x81, 0xC7, 0x03, 0xBF, 0x87, 0x7F,
0x0E, 0x38, 0x1C, 0x70, 0x38, 0xE0, 0x71, 0xC1, 0xE3, 0x9F, 0x87, 0xFE, 0x0F, 0xF0, 0x00, 0x01, 0xC0, 0xFF, 0x0F,
0xF0, 0x1C, 0x01, 0xC3, 0xFC, 0x7F, 0xCF, 0xBC, 0xE1, 0xCE, 0x1C, 0xE1, 0xCE, 0x1C, 0xE1, 0xCF, 0x9C, 0x7F, 0xC3,
0xFC, 0x3F, 0x87, 0xF0, 0x00, 0x7F, 0xEF, 0xFD, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1F, 0xF3, 0xFE, 0x70, 0x0E, 0x01,
0xC0, 0x38, 0x07, 0x00, 0xFF, 0xFF, 0xFC, 0x7F, 0x1F, 0xC0, 0x00, 0x00, 0x3F, 0x1F, 0xEF, 0xFF, 0x87, 0xFF, 0xFF,
0xFE, 0x03, 0x80, 0xF9, 0x9F, 0xE3, 0xF8, 0x3F, 0x07, 0xE0, 0x78, 0x00, 0x0F, 0xFD, 0xFF, 0xB8, 0x07, 0x00, 0xE0,
0x1C, 0x03, 0xFE, 0x7F, 0xCE, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1F, 0xFF, 0xFF, 0x80, 0x37, 0x0F, 0xC1, 0xE0,
0x00, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x3E, 0x67, 0xF8, 0xFE, 0x0E, 0x01, 0xC0,
0x38, 0x00, 0x0F, 0xFD, 0xFF, 0xB8, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0xFE, 0x7F, 0xCE, 0x01, 0xC0, 0x38, 0x07, 0x00,
0xE0, 0x1F, 0xFF, 0xFF, 0x80, 0x0C, 0x07, 0x80, 0xC0, 0x00, 0x00, 0x0F, 0xC7, 0xFB, 0xFF, 0xE1, 0xFF, 0xFF, 0xFF,
0x80, 0xE0, 0x3E, 0x67, 0xF8, 0xFE, 0xFF, 0xDF, 0xFB, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xE7, 0xFC, 0xE0, 0x1C,
0x03, 0x80, 0x70, 0x0E, 0x01, 0xFF, 0xFF, 0xF8, 0x0E, 0x03, 0x80, 0x70, 0x07, 0x80, 0x3F, 0x1F, 0xEF, 0xFF, 0x87,
0xFF, 0xFF, 0xFE, 0x03, 0x80, 0xF9, 0x9F, 0xE3, 0xF8, 0x1C, 0x06, 0x01, 0xE0, 0x33, 0x07, 0xE0, 0x78, 0x06, 0x00,
0x01, 0xFF, 0xBF, 0xF7, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x7F, 0xCF, 0xF9, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03,
0xFF, 0xFF, 0xF0, 0x33, 0x0F, 0xC1, 0xE0, 0x30, 0x00, 0x00, 0x03, 0xF1, 0xFE, 0xFF, 0xF8, 0x7F, 0xFF, 0xFF, 0xE0,
0x38, 0x0F, 0x99, 0xFE, 0x3F, 0x80, 0x03, 0x00, 0x78, 0x0F, 0xC0, 0xCC, 0x00, 0x00, 0xFF, 0x3F, 0xF7, 0xEF, 0x70,
0x0F, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0xEF, 0x3F, 0xF1, 0xFF, 0x0C, 0x07,
0x83, 0xF0, 0xCC, 0x00, 0x0F, 0xF7, 0xFF, 0xE7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFF, 0xF7, 0xFC, 0xFF, 0x01,
0xDD, 0xFF, 0xFB, 0xFC, 0x0D, 0xC0, 0xFC, 0x07, 0x80, 0x00, 0x0F, 0xF3, 0xFF, 0x7E, 0xF7, 0x00, 0xF0, 0x0E, 0x00,
0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0x3B, 0x8F, 0xC1, 0xF0, 0x00,
0x00, 0x0F, 0xF7, 0xFF, 0xE7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFF, 0xF7, 0xFC, 0xFF, 0x01, 0xDD, 0xFF, 0xFB,
0xFC, 0x07, 0x00, 0x78, 0x07, 0x00, 0x00, 0x0F, 0xF3, 0xFF, 0x7E, 0xF7, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00,
0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7E, 0xF3, 0xFF, 0x1F, 0xF0, 0x0E, 0x03, 0x80, 0xE0, 0x00, 0x00, 0x0F, 0xF7,
0xFF, 0xE7, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFF, 0xF7, 0xFC, 0xFF, 0x01, 0xDD, 0xFF, 0xFB, 0xFC, 0x0F, 0xF3,
0xFF, 0x7E, 0xF7, 0x00, 0xF0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7E, 0xF3, 0xFF,
0x1F, 0xF0, 0x00, 0x07, 0x00, 0xF0, 0x0F, 0x00, 0x07, 0x03, 0xC0, 0xE0, 0x38, 0x00, 0x00, 0x03, 0xFD, 0xFF, 0xF9,
0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7F, 0xFD, 0xFF, 0x3F, 0xC0, 0x77, 0x7F, 0xFE, 0xFF, 0x00, 0x06, 0x00, 0xF0,
0x1F, 0x81, 0xD8, 0x00, 0x0E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3F, 0xFF, 0xFF, 0xFE, 0x03, 0xE0,
0x3E, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x03, 0x70, 0x3E, 0x0D, 0xC0, 0x00, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0,
0x3F, 0xCF, 0xFB, 0xDF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0x38, 0x1D, 0xFF, 0xFF, 0xFF,
0xF9, 0xC0, 0xE3, 0x81, 0xC7, 0x03, 0x8F, 0xFF, 0x1F, 0xFE, 0x38, 0x1C, 0x70, 0x38, 0xE0, 0x71, 0xC0, 0xE3, 0x81,
0xC7, 0x03, 0x8E, 0x07, 0x00, 0x70, 0x1F, 0xE3, 0xFC, 0x38, 0x07, 0x00, 0xFF, 0x1F, 0xF3, 0xDF, 0x70, 0xEE, 0x1D,
0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0x7F, 0xFE, 0xFE, 0x00, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x7E, 0xFE, 0xFE, 0x00, 0x00, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
0x38, 0x38, 0x38, 0x38, 0x38, 0xFF, 0xFC, 0x01, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C,
0x38, 0x70, 0xFF, 0xFC, 0x00, 0x03, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x00, 0xDF, 0xFF, 0x80,
0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x00, 0x6E, 0xFD, 0xF0, 0x00, 0x07, 0x0E, 0x1C,
0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0xE7, 0x3C,
0x77, 0x00, 0x07, 0x77, 0x77, 0x77, 0x77, 0x77, 0x7E, 0xEF, 0xEE, 0xE0, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE,
0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8,
0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE7, 0x7F, 0x9F, 0xEE, 0x7F, 0x00, 0xE7, 0xF3, 0xC0, 0x00, 0x00,
0x07, 0x1B, 0x8D, 0xC6, 0xE3, 0x71, 0xB8, 0xDC, 0x6E, 0x37, 0x1B, 0x8D, 0xC6, 0x03, 0x03, 0x87, 0xC3, 0xC0, 0x01,
0x80, 0x78, 0x1F, 0x83, 0x30, 0x00, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00,
0xE0, 0x1C, 0x03, 0x9D, 0xF3, 0xFC, 0x7F, 0x00, 0x0C, 0x1E, 0x3F, 0x33, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x3C, 0xFC, 0xF8, 0xE0, 0xF7, 0x0F, 0x38, 0xF1, 0xC7, 0x0E, 0x70, 0x77, 0x03,
0xF0, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x39, 0xE1, 0xC7, 0x8E, 0x1E, 0x70, 0x7B, 0x81, 0xC0, 0x00, 0x0E, 0x00, 0xF0,
0x07, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0xEE, 0x7B, 0xBC, 0xFE, 0x3F, 0x0F, 0xC3, 0xF8, 0xEF, 0x39,
0xCE, 0x3B, 0x8F, 0x00, 0x07, 0x03, 0xC0, 0xF0, 0xE3, 0xF9, 0xEE, 0xF3, 0xF8, 0xFC, 0x3F, 0x0F, 0xE3, 0xBC, 0xE7,
0xB8, 0xEE, 0x1C, 0x18, 0x0E, 0x07, 0x01, 0x80, 0x00, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0,
0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0F, 0xFF, 0xFF, 0x37, 0xFC, 0x0E, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C,
0xE7, 0xBE, 0xE0, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0,
0x3F, 0xFF, 0xFC, 0x00, 0x0E, 0x07, 0x01, 0xC0, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xE7,
0xCE, 0x01, 0xCF, 0x3C, 0xEE, 0x3B, 0x8E, 0xE3, 0xB8, 0xEE, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80,
0xE0, 0x3F, 0xFF, 0xFC, 0xEF, 0xBE, 0xFB, 0xEF, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0xCF, 0x9C, 0xE0, 0x38, 0x0E,
0x03, 0x80, 0xE0, 0x38, 0x0E, 0x3B, 0x8E, 0xE3, 0xB8, 0x0E, 0x03, 0x80, 0xE0, 0x3F, 0xFF, 0xFC, 0xE1, 0xC3, 0x87,
0x0E, 0x1C, 0x38, 0x77, 0xEF, 0xDF, 0x87, 0x0E, 0x1E, 0x3E, 0x38, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1D, 0x01,
0xF8, 0x1F, 0x07, 0xE0, 0x7C, 0x07, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xFF, 0x1F, 0xF0, 0x30, 0xE1, 0xC3, 0x87,
0x0E, 0x9F, 0x7E, 0xF1, 0xE1, 0xC3, 0x87, 0x07, 0x0F, 0x1E, 0x01, 0x80, 0x1C, 0x01, 0xC0, 0x0C, 0x00, 0x00, 0x70,
0x1F, 0xC0, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFB, 0x8F, 0xCE, 0x7E, 0x7B, 0xF1, 0xDF, 0x87, 0xFC, 0x3F, 0xE0, 0xFF,
0x03, 0xF8, 0x1F, 0xC0, 0x70, 0x07, 0x03, 0xC0, 0xE0, 0x30, 0x00, 0x3F, 0xCF, 0xFB, 0xDF, 0xE1, 0xF8, 0x7E, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE0, 0x3F, 0x81, 0xFE, 0x0F, 0xF8, 0x7F, 0xE3, 0xF7, 0x1F, 0x9C, 0xFC, 0xF7,
0xE3, 0xBF, 0x0F, 0xF8, 0x7F, 0xC1, 0xFE, 0x07, 0xF0, 0x3F, 0x80, 0xE0, 0x00, 0x07, 0x00, 0x78, 0x03, 0x80, 0xFF,
0x3F, 0xEF, 0x7F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1C, 0x00, 0x0E, 0x0F, 0x03, 0xC0, 0x0C,
0xC0, 0x7E, 0x01, 0xE0, 0x06, 0x00, 0x00, 0x70, 0x1F, 0xC0, 0xFF, 0x07, 0xFC, 0x3F, 0xF1, 0xFB, 0x8F, 0xCE, 0x7E,
0x7B, 0xF1, 0xDF, 0x87, 0xFC, 0x3F, 0xE0, 0xFF, 0x03, 0xF8, 0x1F, 0xC0, 0x70, 0x33, 0x0F, 0xC1, 0xE0, 0x30, 0x00,
0x3F, 0xCF, 0xFB, 0xDF, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE0, 0x1C, 0x03, 0x80, 0x60,
0x00, 0x00, 0xFF, 0x1F, 0xF3, 0xDF, 0x70, 0xEE, 0x1D, 0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0xE0, 0x3F,
0x81, 0xFE, 0x0F, 0xF8, 0x7F, 0xE3, 0xF7, 0x1F, 0x9C, 0xFC, 0xF7, 0xE3, 0xBF, 0x0F, 0xF8, 0x3F, 0xC1, 0xFE, 0x07,
0xF0, 0x3F, 0x80, 0xE0, 0x07, 0x00, 0x78, 0x07, 0xC0, 0x3C, 0xFF, 0x3F, 0xEF, 0x7F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1C, 0x07, 0x03, 0xC1, 0xF0, 0x78, 0x0F, 0xE0, 0x1F, 0xC0, 0x00, 0x00, 0xFF, 0x03, 0xFF,
0x0F, 0xDF, 0x1C, 0x0F, 0x78, 0x0E, 0xE0, 0x1D, 0xC0, 0x3B, 0x80, 0x7F, 0x00, 0xEE, 0x01, 0xDE, 0x03, 0x9C, 0x0F,
0x3F, 0x7C, 0x3F, 0xF0, 0x3F, 0xC0, 0x3F, 0x87, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0xFF, 0x3F, 0xF7, 0x0E, 0xE0, 0xFC,
0x1F, 0x83, 0xF0, 0xEF, 0xBC, 0xFF, 0x0F, 0xC0, 0x0E, 0xC0, 0x1F, 0x80, 0x1E, 0x00, 0x00, 0x01, 0xFE, 0x07, 0xFE,
0x1F, 0xBE, 0x38, 0x1E, 0xF0, 0x1D, 0xC0, 0x3B, 0x80, 0x77, 0x00, 0xFE, 0x01, 0xDC, 0x03, 0xBC, 0x07, 0x38, 0x1E,
0x7E, 0xF8, 0x7F, 0xE0, 0x7F, 0x80, 0x3B, 0x07, 0xE0, 0x7C, 0x00, 0x00, 0x00, 0x7E, 0x1F, 0xE7, 0xFE, 0xE1, 0xDC,
0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8, 0x03, 0x70, 0x0E, 0xE0, 0x3B, 0x80, 0x76, 0x00, 0x00, 0x03,
0xFC, 0x0F, 0xFC, 0x3F, 0x7C, 0x70, 0x3D, 0xE0, 0x3B, 0x80, 0x77, 0x00, 0xEE, 0x01, 0xFC, 0x03, 0xB8, 0x07, 0x78,
0x0E, 0x70, 0x3C, 0xFD, 0xF0, 0xFF, 0xC0, 0xFF, 0x00, 0x0D, 0xC3, 0xB8, 0xEE, 0x1D, 0x80, 0x00, 0x7E, 0x1F, 0xE7,
0xFE, 0xE1, 0xDC, 0x1F, 0x83, 0xF0, 0x7E, 0x1D, 0xF7, 0x9F, 0xE1, 0xF8, 0x0F, 0xFF, 0xE3, 0xFF, 0xFE, 0x7E, 0x30,
0x07, 0x83, 0x00, 0xF0, 0x30, 0x0E, 0x03, 0x00, 0xE0, 0x3F, 0xCE, 0x03, 0xFC, 0xE0, 0x30, 0x0E, 0x03, 0x00, 0xF0,
0x30, 0x07, 0x83, 0x00, 0x7E, 0x30, 0x03, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0x3F, 0x3F, 0x1F, 0xFF, 0xEF, 0xFF, 0x3F,
0x87, 0x87, 0xE0, 0xFF, 0xF8, 0x3F, 0xFE, 0x0E, 0x03, 0x87, 0x80, 0xFB, 0xFD, 0x9F, 0xFF, 0xE3, 0xF3, 0xF8, 0x07,
0x00, 0xF0, 0x0E, 0x00, 0xC0, 0x00, 0x0F, 0xF8, 0xFF, 0xCE, 0x3E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEE, 0x3E, 0xFF, 0xCF,
0xF8, 0xE3, 0x8E, 0x3C, 0xE1, 0xCE, 0x0E, 0xE0, 0xFE, 0x07, 0x0C, 0x3C, 0xF1, 0xC0, 0x1F, 0xFF, 0xFB, 0xE1, 0xC3,
0x87, 0x0E, 0x1C, 0x38, 0x70, 0xFF, 0x8F, 0xFC, 0xE3, 0xEE, 0x0E, 0xE0, 0xEE, 0x0E, 0xE3, 0xEF, 0xFC, 0xFF, 0x8E,
0x38, 0xE3, 0xCE, 0x1C, 0xE0, 0xEE, 0x0F, 0xE0, 0x70, 0x00, 0x0E, 0x01, 0xE0, 0x1E, 0x00, 0x7F, 0x7F, 0x7B, 0x70,
0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x00, 0x70, 0xF0, 0xE0, 0x33, 0x03, 0xF0, 0x1E, 0x00, 0xC0, 0x00, 0x0F,
0xF8, 0xFF, 0xCE, 0x3E, 0xE0, 0xEE, 0x0E, 0xE0, 0xEE, 0x3E, 0xFF, 0xCF, 0xF8, 0xE3, 0x8E, 0x3C, 0xE1, 0xCE, 0x0E,
0xE0, 0xFE, 0x07, 0x66, 0xFC, 0xF0, 0xC0, 0x1F, 0xFF, 0xFB, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0x03, 0x00,
0xE0, 0x38, 0x07, 0x00, 0x00, 0x7F, 0x9F, 0xF3, 0xCE, 0x70, 0x0E, 0x01, 0xE0, 0x3F, 0x03, 0xF8, 0x1F, 0x80, 0x78,
0x07, 0x00, 0xEE, 0x7F, 0xFF, 0x3F, 0xC0, 0x06, 0x07, 0x07, 0x03, 0x00, 0x01, 0xF9, 0xFC, 0xF6, 0x70, 0x3E, 0x0F,
0xC1, 0xF0, 0x3B, 0x3D, 0xFE, 0xFE, 0x06, 0x01, 0xE0, 0x7E, 0x0C, 0xC0, 0x00, 0x7F, 0x9F, 0xF3, 0xCE, 0x70, 0x0E,
0x01, 0xE0, 0x3F, 0x03, 0xF8, 0x1F, 0x80, 0x78, 0x07, 0x00, 0xEE, 0x7F, 0xFF, 0x3F, 0xC0, 0x0C, 0x0F, 0x0F, 0xC6,
0x60, 0x01, 0xF9, 0xFC, 0xF6, 0x70, 0x3E, 0x0F, 0xC1, 0xF0, 0x3B, 0x3D, 0xFE, 0xFE, 0x3F, 0xCF, 0xF9, 0xE7, 0x38,
0x07, 0x00, 0xF0, 0x1F, 0x81, 0xFC, 0x0F, 0xC0, 0x3C, 0x03, 0x80, 0x77, 0x3F, 0xFF, 0x9F, 0xE0, 0x70, 0x1E, 0x03,
0xC0, 0x3F, 0x3F, 0x9E, 0xCE, 0x07, 0xC1, 0xF8, 0x3E, 0x07, 0x67, 0xBF, 0xDF, 0xC3, 0x83, 0xE1, 0xE0, 0x19, 0x83,
0xF0, 0x3C, 0x03, 0x00, 0x00, 0x7F, 0x9F, 0xF3, 0xCE, 0x70, 0x0E, 0x01, 0xE0, 0x3F, 0x03, 0xF8, 0x1F, 0x80, 0x78,
0x07, 0x00, 0xEE, 0x7F, 0xFF, 0x3F, 0xC0, 0x33, 0x1F, 0x87, 0x81, 0x80, 0x01, 0xF9, 0xFC, 0xF6, 0x70, 0x3E, 0x0F,
0xC1, 0xF0, 0x3B, 0x3D, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0x01, 0xF0, 0xE0, 0xE0, 0xE0, 0xFE, 0xFE,
0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF6, 0x7E, 0x7E, 0x1C, 0x3C, 0x7C, 0x19, 0x81, 0xF8, 0x0F, 0x00, 0x60, 0x00,
0x0F, 0xFF, 0xFF, 0xF0, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00,
0xE0, 0x0E, 0x00, 0xE0, 0x0C, 0x0C, 0xEC, 0xEC, 0xE0, 0xFE, 0xFE, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xF6, 0x7E,
0x7E, 0xFF, 0xFF, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x03, 0xFC, 0x3F, 0xC0, 0xE0, 0x0E, 0x00, 0xE0,
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0xE0, 0xE0, 0xFE, 0xFE, 0xE0, 0xE0, 0xE0, 0xFE, 0xFE, 0xE0, 0xF6, 0x7E, 0x7E,
0x1E, 0xC1, 0xFC, 0x3F, 0x80, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7D, 0xE3, 0xFE, 0x1F, 0xC0, 0x3F, 0x9F, 0xE7, 0xF0, 0x00, 0x00, 0x38, 0x7E, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0x1F, 0xC1, 0xFC, 0x00, 0x0E, 0x07, 0xE0, 0x7E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC,
0x3F, 0x0F, 0xC0, 0x00, 0x00, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xFB, 0xDF, 0xF3, 0xFC,
0x1D, 0x81, 0xF8, 0x0F, 0x80, 0x00, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7D, 0xE3, 0xFE, 0x1F, 0xC0, 0x3F, 0x0F, 0xC1, 0xE0, 0x00, 0x00, 0x38, 0x7E, 0x1F,
0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0x0F, 0x00, 0xF8, 0x0F, 0x80, 0xF0, 0xE0, 0x7E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7D, 0xE3, 0xFE,
0x1F, 0xC0, 0x1E, 0x0F, 0x83, 0xE0, 0x78, 0x00, 0x38, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFE,
0xF7, 0xFC, 0xFF, 0x06, 0xE0, 0xEE, 0x1D, 0xC1, 0xD8, 0x00, 0x0E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0x70, 0x77, 0xDE, 0x3F, 0xE1, 0xFC, 0x0D, 0x87, 0xF3, 0xB8, 0xCC,
0x00, 0x38, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xFE, 0xF7, 0xFC, 0xFF, 0xE0, 0x7E, 0x07, 0xE0,
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x77, 0x07, 0x7D, 0xE3, 0xFE, 0x1F, 0x80,
0x70, 0x07, 0x00, 0x78, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xE1, 0xF8, 0x7E, 0x1F, 0x87, 0xFB, 0xDF, 0xF3, 0xFC, 0x0E,
0x07, 0x01, 0xF0, 0x00, 0x60, 0x00, 0x1E, 0x00, 0x07, 0xE0, 0x00, 0xCC, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xF8, 0x00,
0x3B, 0x87, 0x07, 0x70, 0xF0, 0xEE, 0x1E, 0x1D, 0xC7, 0xC7, 0x38, 0xFC, 0xE3, 0x9B, 0x9C, 0x77, 0x73, 0x8E, 0xE7,
0x61, 0xD8, 0xFC, 0x1F, 0x0F, 0x83, 0xE1, 0xF0, 0x78, 0x3C, 0x07, 0x03, 0x80, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0,
0x06, 0x60, 0x00, 0x00, 0xE1, 0xC7, 0xE1, 0xC7, 0x73, 0xC7, 0x73, 0xC6, 0x73, 0xEE, 0x37, 0xEE, 0x3F, 0x6E, 0x3E,
0x7C, 0x1E, 0x7C, 0x1E, 0x38, 0x1C, 0x38, 0x06, 0x00, 0x78, 0x07, 0xE0, 0x33, 0x00, 0x00, 0x70, 0x3F, 0xC1, 0xCE,
0x1E, 0x38, 0xE1, 0xCE, 0x07, 0x70, 0x3F, 0x00, 0xF8, 0x07, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0,
0x0E, 0x00, 0x0E, 0x03, 0xC0, 0x7C, 0x1D, 0xC0, 0x01, 0xC3, 0xB8, 0x73, 0x8E, 0x71, 0xCE, 0x70, 0xEE, 0x1D, 0xC1,
0xF0, 0x3E, 0x07, 0xC0, 0x70, 0x0E, 0x17, 0x83, 0xF0, 0x7C, 0x00, 0x1D, 0xC0, 0xEE, 0x00, 0x01, 0xC0, 0xFF, 0x07,
0x38, 0x78, 0xE3, 0x87, 0x38, 0x1D, 0xC0, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07,
0x00, 0x38, 0x00, 0x03, 0x80, 0x78, 0x07, 0x00, 0x60, 0x00, 0x07, 0xFE, 0x7F, 0xE0, 0x0E, 0x01, 0xC0, 0x3C, 0x07,
0x80, 0x70, 0x0E, 0x01, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x07, 0x00, 0xFF, 0xFF, 0xFF, 0x06, 0x03, 0xC1, 0xE0, 0x70,
0x00, 0x1F, 0xE7, 0xF8, 0x1E, 0x0F, 0x03, 0x81, 0xC0, 0xF0, 0x78, 0x1C, 0x0F, 0xFF, 0xFF, 0x07, 0x00, 0xF0, 0x07,
0x00, 0x00, 0x7F, 0xE7, 0xFE, 0x00, 0xE0, 0x1C, 0x03, 0xC0, 0x78, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xC0, 0x38, 0x07,
0x00, 0x70, 0x0F, 0xFF, 0xFF, 0xF0, 0x1C, 0x07, 0x81, 0xC0, 0x00, 0x00, 0x1F, 0xE7, 0xF8, 0x1E, 0x0F, 0x03, 0x81,
0xC0, 0xF0, 0x78, 0x1C, 0x0F, 0xFF, 0xFF, 0x19, 0x81, 0xF8, 0x0F, 0x00, 0x60, 0x00, 0x07, 0xFE, 0x7F, 0xE0, 0x0E,
0x01, 0xC0, 0x3C, 0x07, 0x80, 0x70, 0x0E, 0x01, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x07, 0x00, 0xFF, 0xFF, 0xFF, 0x33,
0x0F, 0xC1, 0xE0, 0x30, 0x00, 0x1F, 0xE7, 0xF8, 0x1E, 0x0F, 0x03, 0x81, 0xC0, 0xF0, 0x78, 0x1C, 0x0F, 0xFF, 0xFF,
0x3F, 0x7E, 0xF8, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x7B, 0xFF, 0xC0,
0x7F, 0xF7, 0xFF, 0x7F, 0xFF, 0xFD, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0xFD, 0xFF, 0xFF, 0xF0, 0x76, 0xEE, 0xE0, 0x77,
0x76, 0xE0, 0x77, 0x76, 0xE0, 0x76, 0x6E, 0xEE, 0xEE, 0xEE, 0x77, 0x77, 0x77, 0x6E, 0xEE, 0x77, 0x77, 0x77, 0x6E,
0xEE, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00,
0xC0, 0x30, 0x0C, 0x03, 0x00, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x3F, 0xFF, 0xFC, 0x30, 0x0C, 0x03, 0x0F, 0xFF,
0xFF, 0x0C, 0x03, 0x00, 0xC0, 0x30, 0x0C, 0x03, 0x00, 0xFB, 0xFF, 0xFF, 0xF8, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x70,
0xE0, 0x7C, 0x38, 0x00, 0xEE, 0x38, 0x00, 0xC6, 0x70, 0x00, 0xC6, 0x70, 0x00, 0xC6, 0xE0, 0x00, 0xEE, 0xE0, 0x00,
0x7D, 0xC0, 0x00, 0x01, 0x80, 0x00, 0x03, 0xBE, 0x3E, 0x07, 0x7F, 0x7F, 0x07, 0x63, 0x67, 0x0E, 0x63, 0x63, 0x0E,
0x63, 0x67, 0x1C, 0x77, 0x77, 0x1C, 0x3E, 0x3E, 0x1C, 0xE3, 0x9C, 0xF1, 0xC3, 0x8E, 0x1C, 0x61, 0xC7, 0x8E, 0x1C,
0xE7, 0x9C, 0x60, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x38, 0x0E, 0x01, 0x80, 0x70, 0x0C, 0x03, 0x80, 0xE0, 0x1C,
0x07, 0x00, 0xE0, 0x38, 0x00, 0x07, 0xF0, 0xFE, 0x1F, 0x61, 0xC0, 0xFF, 0xCF, 0xFC, 0x38, 0x0F, 0xF8, 0xFF, 0x83,
0x80, 0x1C, 0x01, 0xF7, 0x0F, 0xF0, 0x7F, 0xFF, 0xEF, 0xFE, 0x06, 0x00, 0x60, 0x07, 0xC0, 0x7C, 0x3F, 0x83, 0xE0,
0x3F, 0xC0, 0x7C, 0x3F, 0x83, 0xE0, 0x3E, 0x00, 0x60, 0x06, 0x00, 0x3F, 0x83, 0xFC, 0x3B, 0xC0, 0x0E, 0x00, 0xCF,
0xFE, 0x0F, 0x81, 0xF0, 0xFF, 0xE7, 0x00, 0x70, 0x07, 0x00, 0x3D, 0xC3, 0xFE, 0x1F, 0xE0, 0xFF, 0xFF, 0xFC, 0x3C,
0x01, 0xCF, 0xFD, 0xFF, 0x80, 0xE0, 0xF8, 0xFF, 0x1F, 0x83, 0xC0, 0x3C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x3F, 0x83,
0xFC, 0x33, 0xE0, 0x0E, 0x00, 0xE1, 0xFE, 0x3F, 0xF7, 0xDF, 0x70, 0x77, 0x07, 0xE0, 0xEE, 0x0E, 0x70, 0xE7, 0xBC,
0x7F, 0xC3, 0xF8, 0x07, 0x00, 0x1E, 0x00, 0xF8, 0x03, 0xF0, 0x1D, 0xC0, 0x73, 0x81, 0x8E, 0x0E, 0x38, 0x38, 0x70,
0xC1, 0xC7, 0x03, 0x1C, 0x0E, 0xE0, 0x3B, 0xFF, 0xEF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE0, 0x7E, 0x07, 0xE0, 0x7E,
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x70, 0xFF,
0xFF, 0xFD, 0xC0, 0x3C, 0x03, 0x80, 0x38, 0x07, 0x80, 0x70, 0x07, 0x01, 0xC0, 0x70, 0x1E, 0x03, 0x80, 0xE0, 0x3C,
0x07, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xF0, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x38, 0x0E, 0x01, 0x80, 0x70, 0x0C,
0x03, 0x80, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x00, 0xFF, 0x80, 0x00, 0x30, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x06,
0x00, 0x70, 0x03, 0x80, 0x18, 0xF1, 0xC7, 0xCE, 0x3E, 0x60, 0x3B, 0x01, 0xF8, 0x07, 0xC0, 0x3C, 0x00, 0xE0, 0x07,
0x00, 0x7D, 0xEF, 0xFF, 0xE7, 0x3E, 0x73, 0xFF, 0xF7, 0xDE, 0x0F, 0x1F, 0x1E, 0x1C, 0x38, 0x38, 0x38, 0x38, 0x38,
0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x78, 0xF8, 0xF0, 0x3C, 0x6F, 0xFD, 0xFF, 0xB1, 0xE3, 0xC6, 0xFF,
0xDF, 0xFB, 0x1E, 0x03, 0x80, 0xE0, 0x73, 0xFF, 0xFF, 0xC3, 0x80, 0xC0, 0x70, 0xFF, 0xFF, 0xF3, 0x81, 0xC0, 0x70,
0x00, 0x01, 0xC3, 0xF7, 0xFB, 0xF0, 0xF0, 0x3F, 0x07, 0xF8, 0x3F, 0x03, 0xC0, 0x0F, 0xFF, 0xFF, 0xE0, 0x3F, 0x07,
0xF8, 0x3F, 0x03, 0xC7, 0xF7, 0xFB, 0xF0, 0xE0, 0x00, 0x0F, 0xFF, 0xFF,
};
static const EpdGlyph ubuntu_10Glyphs[] = {
{0, 0, 0, 0, 0, 0, 0}, //
{0, 0, 0, 0, 0, 0, 0}, // 
{0, 0, 5, 0, 0, 0, 0}, //
{0, 0, 5, 0, 0, 0, 0}, //
{0, 0, 0, 0, 0, 0, 0}, // 
{0, 0, 5, 0, 0, 0, 0}, //
{4, 15, 6, 1, 15, 8, 0}, // !
{7, 5, 9, 1, 16, 5, 8}, // "
{12, 15, 14, 1, 15, 23, 13}, // #
{10, 19, 12, 1, 16, 24, 36}, // $
{16, 15, 18, 1, 15, 30, 60}, // %
{14, 15, 14, 0, 15, 27, 90}, // &
{3, 5, 5, 1, 16, 2, 117}, // '
{6, 20, 7, 1, 16, 15, 119}, // (
{6, 20, 7, 0, 16, 15, 134}, // )
{10, 8, 10, 0, 15, 10, 149}, // *
{10, 10, 12, 1, 11, 13, 159}, // +
{4, 5, 5, 0, 2, 3, 172}, // ,
{6, 2, 6, 0, 7, 2, 175}, // -
{3, 3, 5, 1, 3, 2, 177}, // .
{10, 20, 8, -1, 16, 25, 179}, // /
{10, 15, 12, 1, 15, 19, 204}, // 0
{7, 15, 12, 1, 15, 14, 223}, // 1
{10, 15, 12, 1, 15, 19, 237}, // 2
{10, 15, 12, 1, 15, 19, 256}, // 3
{11, 15, 12, 0, 15, 21, 275}, // 4
{10, 15, 12, 1, 15, 19, 296}, // 5
{10, 15, 12, 1, 15, 19, 315}, // 6
{10, 15, 12, 1, 15, 19, 334}, // 7
{10, 15, 12, 1, 15, 19, 353}, // 8
{10, 15, 12, 1, 15, 19, 372}, // 9
{3, 11, 5, 1, 11, 5, 391}, // :
{4, 14, 5, 0, 11, 7, 396}, // ;
{10, 9, 12, 1, 11, 12, 403}, // <
{10, 7, 12, 1, 10, 9, 415}, // =
{10, 9, 12, 1, 11, 12, 424}, // >
{8, 15, 8, 0, 15, 15, 436}, // ?
{18, 20, 20, 1, 16, 45, 451}, // @
{14, 15, 14, 0, 15, 27, 496}, // A
{12, 15, 14, 1, 15, 23, 523}, // B
{12, 15, 13, 1, 15, 23, 546}, // C
{13, 15, 15, 1, 15, 25, 569}, // D
{11, 15, 12, 1, 15, 21, 594}, // E
{10, 15, 11, 1, 15, 19, 615}, // F
{12, 15, 14, 1, 15, 23, 634}, // G
{12, 15, 15, 1, 15, 23, 657}, // H
{3, 15, 6, 1, 15, 6, 680}, // I
{9, 15, 11, 0, 15, 17, 686}, // J
{13, 15, 13, 1, 15, 25, 703}, // K
{10, 15, 11, 1, 15, 19, 728}, // L
{16, 15, 18, 1, 15, 30, 747}, // M
{13, 15, 15, 1, 15, 25, 777}, // N
{15, 15, 16, 1, 15, 29, 802}, // O
{12, 15, 13, 1, 15, 23, 831}, // P
{15, 19, 16, 1, 15, 36, 854}, // Q
{12, 15, 13, 1, 15, 23, 890}, // R
{11, 15, 11, 0, 15, 21, 913}, // S
{12, 15, 12, 0, 15, 23, 934}, // T
{12, 15, 14, 1, 15, 23, 957}, // U
{14, 15, 14, 0, 15, 27, 980}, // V
{19, 15, 20, 0, 15, 36, 1007}, // W
{13, 15, 13, 0, 15, 25, 1043}, // X
{13, 15, 13, 0, 15, 25, 1068}, // Y
{12, 15, 12, 0, 15, 23, 1093}, // Z
{5, 20, 7, 2, 16, 13, 1116}, // [
{10, 20, 8, -1, 16, 25, 1129}, // <backslash>
{5, 20, 7, 0, 16, 13, 1154}, // ]
{11, 8, 12, 0, 15, 11, 1167}, // ^
{12, 2, 10, -1, -2, 3, 1178}, // _
{5, 4, 8, 1, 16, 3, 1181}, // `
{10, 11, 11, 0, 11, 14, 1184}, // a
{11, 16, 12, 1, 16, 22, 1198}, // b
{9, 11, 10, 1, 11, 13, 1220}, // c
{10, 16, 12, 1, 16, 20, 1233}, // d
{10, 11, 12, 1, 11, 14, 1253}, // e
{8, 16, 8, 1, 16, 16, 1267}, // f
{10, 15, 12, 1, 11, 19, 1283}, // g
{10, 16, 12, 1, 16, 20, 1302}, // h
{3, 16, 5, 1, 16, 6, 1322}, // i
{6, 20, 5, -2, 16, 15, 1328}, // j
{10, 16, 11, 1, 16, 20, 1343}, // k
{5, 16, 6, 1, 16, 10, 1363}, // l
{16, 11, 18, 1, 11, 22, 1373}, // m
{10, 11, 12, 1, 11, 14, 1395}, // n
{11, 11, 12, 1, 11, 16, 1409}, // o
{11, 15, 12, 1, 11, 21, 1425}, // p
{10, 15, 12, 1, 11, 19, 1446}, // q
{7, 11, 8, 1, 11, 10, 1465}, // r
{9, 11, 9, 0, 11, 13, 1475}, // s
{8, 14, 8, 1, 14, 14, 1488}, // t
{10, 11, 12, 1, 11, 14, 1502}, // u
{11, 11, 11, 0, 11, 16, 1516}, // v
{16, 11, 16, 0, 11, 22, 1532}, // w
{11, 11, 11, 0, 11, 16, 1554}, // x
{11, 15, 10, 0, 11, 21, 1570}, // y
{10, 11, 10, 0, 11, 14, 1591}, // z
{7, 20, 7, 0, 16, 18, 1605}, // {
{2, 20, 6, 2, 16, 5, 1623}, // |
{7, 20, 7, 0, 16, 18, 1628}, // }
{11, 4, 12, 0, 8, 6, 1646}, // ~
{0, 0, 5, 0, 0, 0, 1652}, //  
{4, 15, 6, 1, 11, 8, 1652}, // ¡
{10, 17, 12, 1, 14, 22, 1660}, // ¢
{10, 15, 12, 1, 15, 19, 1682}, // £
{11, 10, 12, 0, 12, 14, 1701}, // ¤
{12, 15, 12, 0, 15, 23, 1715}, // ¥
{2, 20, 6, 2, 16, 5, 1738}, // ¦
{10, 18, 10, 0, 16, 23, 1743}, // §
{8, 2, 8, 0, 15, 2, 1766}, // ¨
{15, 15, 17, 1, 15, 29, 1768}, // ©
{8, 8, 8, 0, 15, 8, 1797}, // ª
{10, 9, 10, 0, 10, 12, 1805}, // «
{10, 8, 12, 1, 9, 10, 1817}, // ¬
{6, 2, 6, 0, 7, 2, 1827}, // ­
{15, 15, 17, 1, 15, 29, 1829}, // ®
{7, 2, 8, 0, 15, 2, 1858}, // ¯
{7, 6, 7, 0, 16, 6, 1860}, // °
{10, 13, 12, 1, 13, 17, 1866}, // ±
{7, 8, 8, 0, 15, 7, 1883}, // ²
{7, 8, 8, 0, 15, 7, 1890}, // ³
{5, 4, 8, 2, 16, 3, 1897}, // ´
{10, 15, 12, 1, 11, 19, 1900}, // µ
{12, 19, 14, 0, 15, 29, 1919}, // ¶
{3, 3, 5, 1, 8, 2, 1948}, // ·
{5, 4, 8, 1, 0, 3, 1950}, // ¸
{4, 8, 8, 1, 15, 4, 1953}, // ¹
{9, 8, 9, 0, 15, 9, 1957}, // º
{10, 9, 10, 0, 10, 12, 1966}, // »
{17, 15, 18, 1, 15, 32, 1978}, // ¼
{17, 15, 18, 1, 15, 32, 2010}, // ½
{18, 15, 18, 0, 15, 34, 2042}, // ¾
{9, 15, 8, 0, 11, 17, 2076}, // ¿
{14, 20, 14, 0, 20, 35, 2093}, // À
{14, 20, 14, 0, 20, 35, 2128}, // Á
{14, 20, 14, 0, 20, 35, 2163}, // Â
{14, 19, 14, 0, 19, 34, 2198}, // Ã
{14, 18, 14, 0, 18, 32, 2232}, // Ä
{14, 18, 14, 0, 18, 32, 2264}, // Å
{19, 15, 20, 0, 15, 36, 2296}, // Æ
{12, 18, 13, 1, 15, 27, 2332}, // Ç
{11, 20, 12, 1, 20, 28, 2359}, // È
{11, 20, 12, 1, 20, 28, 2387}, // É
{11, 20, 12, 1, 20, 28, 2415}, // Ê
{11, 18, 12, 1, 18, 25, 2443}, // Ë
{5, 20, 6, 0, 20, 13, 2468}, // Ì
{5, 20, 6, 1, 20, 13, 2481}, // Í
{7, 20, 6, -1, 20, 18, 2494}, // Î
{8, 18, 6, -1, 18, 18, 2512}, // Ï
{15, 15, 15, 0, 15, 29, 2530}, // Ð
{13, 19, 15, 1, 19, 31, 2559}, // Ñ
{15, 20, 16, 1, 20, 38, 2590}, // Ò
{15, 20, 16, 1, 20, 38, 2628}, // Ó
{15, 20, 16, 1, 20, 38, 2666}, // Ô
{15, 19, 16, 1, 19, 36, 2704}, // Õ
{15, 18, 16, 1, 18, 34, 2740}, // Ö
{10, 9, 12, 1, 11, 12, 2774}, // ×
{15, 15, 16, 1, 15, 29, 2786}, // Ø
{12, 20, 14, 1, 20, 30, 2815}, // Ù
{12, 20, 14, 1, 20, 30, 2845}, // Ú
{12, 20, 14, 1, 20, 30, 2875}, // Û
{12, 18, 14, 1, 18, 27, 2905}, // Ü
{13, 20, 13, 0, 20, 33, 2932}, // Ý
{12, 15, 13, 1, 15, 23, 2965}, // Þ
{12, 16, 13, 1, 16, 24, 2988}, // ß
{10, 16, 11, 0, 16, 20, 3012}, // à
{10, 16, 11, 0, 16, 20, 3032}, // á
{10, 16, 11, 0, 16, 20, 3052}, // â
{10, 16, 11, 0, 16, 20, 3072}, // ã
{10, 15, 11, 0, 15, 19, 3092}, // ä
{10, 16, 11, 0, 16, 20, 3111}, // å
{17, 11, 18, 0, 11, 24, 3131}, // æ
{9, 15, 10, 1, 11, 17, 3155}, // ç
{10, 16, 12, 1, 16, 20, 3172}, // è
{10, 16, 12, 1, 16, 20, 3192}, // é
{10, 16, 12, 1, 16, 20, 3212}, // ê
{10, 15, 12, 1, 15, 19, 3232}, // ë
{5, 16, 5, 0, 16, 10, 3251}, // ì
{5, 16, 5, 1, 16, 10, 3261}, // í
{7, 16, 5, -1, 16, 14, 3271}, // î
{7, 15, 5, -1, 15, 14, 3285}, // ï
{11, 16, 12, 1, 16, 22, 3299}, // ð
{10, 16, 12, 1, 16, 20, 3321}, // ñ
{11, 16, 12, 1, 16, 22, 3341}, // ò
{11, 16, 12, 1, 16, 22, 3363}, // ó
{11, 16, 12, 1, 16, 22, 3385}, // ô
{11, 16, 12, 1, 16, 22, 3407}, // õ
{11, 15, 12, 1, 15, 21, 3429}, // ö
{10, 12, 12, 1, 12, 15, 3450}, // ÷
{11, 11, 12, 1, 11, 16, 3465}, // ø
{10, 16, 12, 1, 16, 20, 3481}, // ù
{10, 16, 12, 1, 16, 20, 3501}, // ú
{10, 16, 12, 1, 16, 20, 3521}, // û
{10, 15, 12, 1, 15, 19, 3541}, // ü
{11, 20, 10, 0, 16, 28, 3560}, // ý
{11, 20, 12, 1, 16, 28, 3588}, // þ
{11, 19, 10, 0, 15, 27, 3616}, // ÿ
{14, 18, 14, 0, 18, 32, 3643}, // Ā
{10, 15, 11, 0, 15, 19, 3675}, // ā
{14, 19, 14, 0, 19, 34, 3694}, // Ă
{10, 16, 11, 0, 16, 20, 3728}, // ă
{14, 19, 14, 0, 15, 34, 3748}, // Ą
{10, 14, 11, 0, 11, 18, 3782}, // ą
{12, 20, 13, 1, 20, 30, 3800}, // Ć
{9, 16, 10, 1, 16, 18, 3830}, // ć
{12, 20, 13, 1, 20, 30, 3848}, // Ĉ
{9, 16, 10, 1, 16, 18, 3878}, // ĉ
{12, 19, 13, 1, 19, 29, 3896}, // Ċ
{9, 16, 10, 1, 16, 18, 3925}, // ċ
{12, 20, 13, 1, 20, 30, 3943}, // Č
{9, 16, 10, 1, 16, 18, 3973}, // č
{13, 20, 15, 1, 20, 33, 3991}, // Ď
{14, 16, 13, 1, 16, 28, 4024}, // ď
{15, 15, 15, 0, 15, 29, 4052}, // Đ
{12, 16, 12, 1, 16, 24, 4081}, // đ
{11, 18, 12, 1, 18, 25, 4105}, // Ē
{10, 15, 12, 1, 15, 19, 4130}, // ē
{11, 19, 12, 1, 19, 27, 4149}, // Ĕ
{10, 16, 12, 1, 16, 20, 4176}, // ĕ
{11, 19, 12, 1, 19, 27, 4196}, // Ė
{10, 16, 12, 1, 16, 20, 4223}, // ė
{11, 19, 12, 1, 15, 27, 4243}, // Ę
{10, 14, 12, 1, 11, 18, 4270}, // ę
{11, 20, 12, 1, 20, 28, 4288}, // Ě
{10, 17, 12, 1, 17, 22, 4316}, // ě
{12, 20, 14, 1, 20, 30, 4338}, // Ĝ
{10, 20, 12, 1, 16, 25, 4368}, // ĝ
{12, 19, 14, 1, 19, 29, 4393}, // Ğ
{10, 20, 12, 1, 16, 25, 4422}, // ğ
{12, 19, 14, 1, 19, 29, 4447}, // Ġ
{10, 20, 12, 1, 16, 25, 4476}, // ġ
{12, 19, 14, 1, 15, 29, 4501}, // Ģ
{10, 21, 12, 1, 17, 27, 4530}, // ģ
{12, 20, 15, 1, 20, 30, 4557}, // Ĥ
{10, 20, 12, 1, 20, 25, 4587}, // ĥ
{15, 15, 15, 0, 15, 29, 4612}, // Ħ
{11, 16, 12, 0, 16, 22, 4641}, // ħ
{8, 19, 6, -1, 19, 19, 4663}, // Ĩ
{8, 16, 5, -1, 16, 16, 4682}, // ĩ
{7, 18, 6, -1, 18, 16, 4698}, // Ī
{7, 15, 5, -1, 15, 14, 4714}, // ī
{6, 19, 6, 0, 19, 15, 4728}, // Ĭ
{7, 16, 5, -1, 16, 14, 4743}, // ĭ
{5, 19, 6, 0, 15, 12, 4757}, // Į
{4, 20, 5, 0, 16, 10, 4769}, // į
{4, 19, 6, 1, 19, 10, 4779}, // İ
{3, 11, 5, 1, 11, 5, 4789}, // ı
{14, 15, 16, 1, 15, 27, 4794}, // IJ
{9, 20, 11, 1, 16, 23, 4821}, // ij
{11, 20, 11, 0, 20, 28, 4844}, // Ĵ
{8, 20, 5, -2, 16, 20, 4872}, // ĵ
{13, 19, 13, 1, 15, 31, 4892}, // Ķ
{10, 20, 11, 1, 16, 25, 4923}, // ķ
{10, 11, 11, 1, 11, 14, 4948}, // ĸ
{10, 20, 11, 1, 20, 25, 4962}, // Ĺ
{5, 20, 6, 1, 20, 13, 4987}, // ĺ
{10, 19, 11, 1, 15, 24, 5000}, // Ļ
{6, 20, 6, 0, 16, 15, 5024}, // ļ
{10, 15, 11, 1, 15, 19, 5039}, // Ľ
{6, 16, 6, 1, 16, 12, 5058}, // ľ
{10, 15, 11, 1, 15, 19, 5070}, // Ŀ
{7, 16, 8, 1, 16, 14, 5089}, // ŀ
{12, 15, 11, -1, 15, 23, 5103}, // Ł
{7, 16, 6, 0, 16, 14, 5126}, // ł
{13, 20, 15, 1, 20, 33, 5140}, // Ń
{10, 16, 12, 1, 16, 20, 5173}, // ń
{13, 19, 15, 1, 15, 31, 5193}, // Ņ
{10, 15, 12, 1, 11, 19, 5224}, // ņ
{13, 20, 15, 1, 20, 33, 5243}, // Ň
{10, 16, 12, 1, 16, 20, 5276}, // ň
{11, 16, 12, 0, 16, 22, 5296}, // ʼn
{13, 19, 15, 1, 15, 31, 5318}, // Ŋ
{10, 15, 12, 1, 11, 19, 5349}, // ŋ
{15, 18, 16, 1, 18, 34, 5368}, // Ō
{11, 15, 12, 1, 15, 21, 5402}, // ō
{15, 19, 16, 1, 19, 36, 5423}, // Ŏ
{11, 16, 12, 1, 16, 22, 5459}, // ŏ
{15, 20, 16, 1, 20, 38, 5481}, // Ő
{11, 16, 12, 1, 16, 22, 5519}, // ő
{20, 15, 21, 1, 15, 38, 5541}, // Œ
{18, 11, 20, 1, 11, 25, 5579}, // œ
{12, 20, 13, 1, 20, 30, 5604}, // Ŕ
{7, 16, 8, 1, 16, 14, 5634}, // ŕ
{12, 19, 13, 1, 15, 29, 5648}, // Ŗ
{8, 15, 8, 0, 11, 15, 5677}, // ŗ
{12, 20, 13, 1, 20, 30, 5692}, // Ř
{7, 16, 8, 1, 16, 14, 5722}, // ř
{11, 20, 11, 0, 20, 28, 5736}, // Ś
{9, 16, 9, 0, 16, 18, 5764}, // ś
{11, 20, 11, 0, 20, 28, 5782}, // Ŝ
{9, 16, 9, 0, 16, 18, 5810}, // ŝ
{11, 18, 11, 0, 15, 25, 5828}, // Ş
{9, 14, 9, 0, 11, 16, 5853}, // ş
{11, 20, 11, 0, 20, 28, 5869}, // Š
{9, 16, 9, 0, 16, 18, 5897}, // š
{12, 18, 12, 0, 15, 27, 5915}, // Ţ
{8, 17, 8, 1, 14, 17, 5942}, // ţ
{12, 20, 12, 0, 20, 30, 5959}, // Ť
{8, 16, 8, 1, 16, 16, 5989}, // ť
{12, 15, 12, 0, 15, 23, 6005}, // Ŧ
{8, 14, 8, 1, 14, 14, 6028}, // ŧ
{12, 19, 14, 1, 19, 29, 6042}, // Ũ
{10, 16, 12, 1, 16, 20, 6071}, // ũ
{12, 18, 14, 1, 18, 27, 6091}, // Ū
{10, 15, 12, 1, 15, 19, 6118}, // ū
{12, 19, 14, 1, 19, 29, 6137}, // Ŭ
{10, 16, 12, 1, 16, 20, 6166}, // ŭ
{12, 19, 14, 1, 19, 29, 6186}, // Ů
{10, 16, 12, 1, 16, 20, 6215}, // ů
{12, 20, 14, 1, 20, 30, 6235}, // Ű
{10, 16, 12, 1, 16, 20, 6265}, // ű
{12, 18, 14, 1, 15, 27, 6285}, // Ų
{10, 14, 12, 1, 11, 18, 6312}, // ų
{19, 20, 20, 0, 20, 48, 6330}, // Ŵ
{16, 16, 16, 0, 16, 32, 6378}, // ŵ
{13, 20, 13, 0, 20, 33, 6410}, // Ŷ
{11, 20, 10, 0, 16, 28, 6443}, // ŷ
{13, 18, 13, 0, 18, 30, 6471}, // Ÿ
{12, 20, 12, 0, 20, 30, 6501}, // Ź
{10, 16, 10, 0, 16, 20, 6531}, // ź
{12, 19, 12, 0, 19, 29, 6551}, // Ż
{10, 16, 10, 0, 16, 20, 6580}, // ż
{12, 20, 12, 0, 20, 30, 6600}, // Ž
{10, 16, 10, 0, 16, 20, 6630}, // ž
{8, 16, 6, 1, 16, 16, 6650}, // ſ
{6, 3, 8, 1, 16, 3, 6666}, // ̑
{12, 2, 10, -1, 7, 3, 6669}, //
{22, 2, 21, -1, 7, 6, 6672}, // —
{22, 2, 21, -1, 7, 6, 6678}, // ―
{4, 5, 5, 1, 16, 3, 6684}, //
{4, 5, 5, 0, 16, 3, 6687}, //
{4, 5, 5, 0, 2, 3, 6690}, //
{8, 5, 9, 1, 16, 5, 6693}, // “
{8, 5, 9, 0, 16, 5, 6698}, // ”
{8, 5, 9, 0, 2, 5, 6703}, // „
{10, 18, 10, 0, 15, 23, 6708}, // †
{10, 18, 10, 0, 15, 23, 6731}, // ‡
{6, 5, 8, 1, 10, 4, 6754}, // •
{17, 3, 21, 2, 3, 7, 6758}, // …
{24, 15, 26, 1, 15, 45, 6765}, // ‰
{6, 9, 6, 0, 10, 7, 6810}, //
{6, 9, 6, 0, 10, 7, 6817}, //
{11, 15, 3, -4, 15, 21, 6824}, //
{12, 14, 12, 0, 14, 21, 6845}, // €
{12, 15, 12, 0, 15, 23, 6866}, // ₮
{12, 15, 12, 0, 15, 23, 6889}, // ₴
{11, 15, 12, 1, 15, 21, 6912}, // ₹
{12, 16, 12, 0, 16, 24, 6933}, // ∂
{14, 15, 14, 0, 15, 27, 6957}, // ∆
{12, 17, 15, 1, 15, 26, 6984}, // ∏
{11, 17, 11, 0, 15, 24, 7010}, // ∑
{10, 2, 12, 1, 7, 3, 7034}, //
{11, 15, 3, -4, 15, 21, 7037}, //
{3, 3, 5, 1, 8, 2, 7058}, // ∙
{13, 17, 12, 0, 16, 28, 7060}, // √
{12, 6, 12, 0, 9, 9, 7088}, // ∞
{8, 20, 8, 0, 16, 20, 7097}, // ∫
{11, 8, 12, 0, 10, 11, 7117}, // ≈
{10, 13, 12, 1, 13, 17, 7128}, // ≠
{10, 12, 12, 1, 12, 15, 7145}, // ≤
{10, 12, 12, 1, 12, 15, 7160}, // ≥
};
static const EpdUnicodeInterval ubuntu_10Intervals[] = {
{0x0, 0x0, 0x0}, {0x8, 0x9, 0x1}, {0xD, 0xD, 0x3}, {0x1D, 0x1D, 0x4},
{0x20, 0x7E, 0x5}, {0xA0, 0xFF, 0x64}, {0x100, 0x17F, 0xC4}, {0x311, 0x311, 0x144},
{0x2013, 0x2015, 0x145}, {0x2018, 0x201A, 0x148}, {0x201C, 0x201E, 0x14B}, {0x2020, 0x2022, 0x14E},
{0x2026, 0x2026, 0x151}, {0x2030, 0x2030, 0x152}, {0x2039, 0x203A, 0x153}, {0x2044, 0x2044, 0x155},
{0x20AC, 0x20AC, 0x156}, {0x20AE, 0x20AE, 0x157}, {0x20B4, 0x20B4, 0x158}, {0x20B9, 0x20B9, 0x159},
{0x2202, 0x2202, 0x15A}, {0x2206, 0x2206, 0x15B}, {0x220F, 0x220F, 0x15C}, {0x2211, 0x2212, 0x15D},
{0x2215, 0x2215, 0x15F}, {0x2219, 0x221A, 0x160}, {0x221E, 0x221E, 0x162}, {0x222B, 0x222B, 0x163},
{0x2248, 0x2248, 0x164}, {0x2260, 0x2260, 0x165}, {0x2264, 0x2265, 0x166},
};
static const EpdFontData ubuntu_10 = {
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4,
};

View File

@@ -1,810 +0,0 @@
/**
* generated by fontconvert.py
* name: ubuntu_bold_10
* size: 10
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t ubuntu_bold_10Bitmaps[8017] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xEF, 0xEF, 0xE7, 0x1E, 0xF0, 0xF7, 0x87, 0x3C,
0x39, 0xCF, 0xFF, 0xFF, 0xFC, 0xF7, 0x87, 0xBC, 0x39, 0xE7, 0xFF, 0xFF, 0xFE, 0xF7, 0x87, 0xBC, 0x39, 0xE1, 0xCE,
0x00, 0x0F, 0x01, 0xE0, 0xFF, 0xBF, 0xF7, 0xFC, 0xFB, 0x9E, 0x03, 0xF8, 0x3F, 0xC3, 0xFC, 0x1F, 0x80, 0xF7, 0xBE,
0xFF, 0xFF, 0xFB, 0xFE, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x3E, 0x1E, 0x0F, 0xE3, 0xC1, 0xDC, 0xF0, 0x7B, 0xDE, 0x0F,
0x7F, 0x80, 0xEE, 0xF0, 0x1F, 0xFC, 0x01, 0xF7, 0xFE, 0x01, 0xFF, 0xC0, 0x3B, 0xFC, 0x0F, 0x73, 0x83, 0xCE, 0x70,
0x79, 0xFE, 0x1E, 0x3F, 0x83, 0xC3, 0xF0, 0x1F, 0x80, 0x7F, 0x80, 0xFF, 0x81, 0xEF, 0x03, 0xDE, 0x07, 0xF8, 0x07,
0xF0, 0x1F, 0xDE, 0x7F, 0xFC, 0xE7, 0xF3, 0xC7, 0xE7, 0xE7, 0xC7, 0xFF, 0x8F, 0xFF, 0x8F, 0xFF, 0x80, 0xFF, 0xFE,
0xEE, 0x1C, 0x3C, 0xF3, 0xE7, 0x8F, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x1E, 0x3C, 0x7C, 0x78, 0x78, 0xE0,
0x63, 0xCF, 0x9E, 0x78, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xDE, 0x7B, 0xEF, 0x18, 0x1E, 0x03, 0x87, 0xF9,
0xFF, 0x7F, 0xCF, 0xE7, 0xF8, 0xEC, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x81,
0xE0, 0x7B, 0xDF, 0xEF, 0x78, 0xFF, 0xFF, 0xF8, 0x77, 0xFE, 0xE0, 0x01, 0xE0, 0x3C, 0x0F, 0x01, 0xE0, 0x7C, 0x0F,
0x01, 0xE0, 0x7C, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x01, 0xE0, 0x78,
0x00, 0x1F, 0x83, 0xFC, 0x7F, 0xE7, 0xFE, 0x79, 0xEF, 0x1F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x1F, 0x79, 0xE7, 0xFE,
0x7F, 0xE3, 0xFC, 0x1F, 0x80, 0x0F, 0x1F, 0x3F, 0xFF, 0xFF, 0x7F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
0x0F, 0x3F, 0x8F, 0xF9, 0xFF, 0xBD, 0xF2, 0x1E, 0x07, 0xC0, 0xF0, 0x3C, 0x0F, 0x83, 0xC0, 0xF0, 0x3E, 0x07, 0xFE,
0xFF, 0xDF, 0xF8, 0x3F, 0x8F, 0xF9, 0xFF, 0x3B, 0xE0, 0x3C, 0x0F, 0x87, 0xE0, 0xFE, 0x07, 0xE0, 0x3C, 0x07, 0xBB,
0xFF, 0xFF, 0xFF, 0xBF, 0xE0, 0x03, 0xC0, 0x7C, 0x0F, 0xC1, 0xFC, 0x1F, 0xC3, 0xFC, 0x7B, 0xC7, 0xBC, 0xF3, 0xCF,
0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3F, 0xE7, 0xFC, 0xFF, 0x9C, 0x03, 0x80, 0xFE, 0x1F, 0xF3,
0xFF, 0x1F, 0xE0, 0x3C, 0x07, 0xBB, 0xF7, 0xFF, 0xFF, 0x9F, 0xE0, 0x07, 0xE0, 0xFE, 0x3F, 0xE3, 0xF8, 0x7C, 0x07,
0xFC, 0x7F, 0xEF, 0xFE, 0xFB, 0xEF, 0x0F, 0xF8, 0xF7, 0xFE, 0x7F, 0xE3, 0xFC, 0x1F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x3E, 0x03, 0xC0, 0x7C, 0x07, 0x80, 0xF8, 0x0F, 0x00, 0xF0, 0x1E, 0x01, 0xE0, 0x1E, 0x01, 0xE0, 0x1E, 0x00,
0x1F, 0x83, 0xFC, 0x7F, 0xE7, 0xFE, 0x79, 0xE7, 0x9E, 0x7F, 0xC3, 0xFC, 0x7F, 0xE7, 0xBE, 0xF0, 0xFF, 0x9F, 0x7F,
0xE7, 0xFE, 0x3F, 0xC0, 0x3F, 0x87, 0xFC, 0x7F, 0xEF, 0xBE, 0xF1, 0xEF, 0x1E, 0xFD, 0xE7, 0xFE, 0x7F, 0xE3, 0xFE,
0x03, 0xE1, 0xFC, 0x7F, 0xC7, 0xF8, 0x7E, 0x00, 0x77, 0xFE, 0xE0, 0x00, 0x0E, 0xFF, 0xDC, 0x77, 0xFE, 0xE0, 0x00,
0x00, 0x7B, 0xDF, 0xEF, 0x78, 0x01, 0xC3, 0xF3, 0xFF, 0xFE, 0xFE, 0x3E, 0x0F, 0xE3, 0xFE, 0x3F, 0xC3, 0xF0, 0x1C,
0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0x0F, 0xF3, 0xFF, 0x1F, 0xC1, 0xF1, 0xFF,
0xFF, 0xFF, 0x3F, 0x0E, 0x00, 0x7F, 0x3F, 0xEF, 0xF9, 0xDE, 0x07, 0x81, 0xE0, 0xF8, 0x7C, 0x1E, 0x0F, 0x03, 0xC0,
0x00, 0x3C, 0x0F, 0x83, 0xE0, 0xF0, 0x07, 0xFC, 0x03, 0xFF, 0xE0, 0xFE, 0xFE, 0x3E, 0x03, 0xE7, 0xBF, 0xFD, 0xEF,
0xFB, 0xB9, 0xFF, 0x7F, 0x79, 0xEF, 0xEF, 0x3C, 0xFD, 0xE7, 0xBF, 0xBC, 0xF7, 0xF7, 0xDE, 0xEF, 0x7F, 0xFD, 0xE7,
0xFF, 0x1F, 0x00, 0x01, 0xFC, 0xE0, 0x1F, 0xFC, 0x00, 0xFF, 0x80, 0x07, 0xC0, 0x0F, 0x80, 0x3F, 0x80, 0x7F, 0x00,
0xFF, 0x03, 0xDE, 0x07, 0xBC, 0x1F, 0x3C, 0x3C, 0x78, 0x7F, 0xF1, 0xFF, 0xF3, 0xFF, 0xE7, 0x83, 0xFF, 0x07, 0xFC,
0x07, 0x80, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xE3, 0xEF, 0x1F, 0x7F, 0xF3, 0xFF, 0x9F, 0xFE, 0xF1, 0xF7, 0x87, 0xFC,
0x3F, 0xE7, 0xEF, 0xFF, 0x7F, 0xF3, 0xFF, 0x00, 0x1F, 0xF1, 0xFF, 0x9F, 0xFC, 0xFD, 0xEF, 0x80, 0x78, 0x03, 0xC0,
0x1E, 0x00, 0xF0, 0x07, 0x80, 0x3E, 0x01, 0xFC, 0xE7, 0xFF, 0x1F, 0xF8, 0x7F, 0xC0, 0xFF, 0xC3, 0xFF, 0xCF, 0xFF,
0x3C, 0xFE, 0xF0, 0x7B, 0xC1, 0xFF, 0x03, 0xFC, 0x0F, 0xF0, 0x3F, 0xC1, 0xFF, 0x07, 0xBC, 0xFE, 0xFF, 0xF3, 0xFF,
0x8F, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3F, 0xF7, 0xFE, 0xFF, 0xDE, 0x03, 0xC0, 0x78,
0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0x01, 0xE0, 0x3F, 0xF7, 0xFE, 0xFF, 0xDE, 0x03,
0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x1F, 0xF1, 0xFF, 0x9F, 0xFC, 0xFC, 0xEF, 0x80, 0x78, 0x03, 0xC0, 0x1E,
0x0F, 0xF0, 0x7F, 0x83, 0xFE, 0x1F, 0xFC, 0xF7, 0xFF, 0x9F, 0xFC, 0x7F, 0xE0, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0,
0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xE0,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03,
0xC0, 0xF0, 0x3D, 0x9F, 0xFF, 0xFF, 0xEF, 0xF0, 0xF0, 0xFB, 0xC7, 0xCF, 0x3E, 0x3D, 0xF0, 0xFF, 0x83, 0xFC, 0x0F,
0xE0, 0x3F, 0xC0, 0xFF, 0x83, 0xDF, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xF3, 0xC3, 0xEF, 0x0F, 0x80, 0xF0, 0x1E, 0x03,
0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0x78,
0x0F, 0x3E, 0x0F, 0x9F, 0x07, 0xCF, 0xC7, 0xEF, 0xE3, 0xF7, 0xFB, 0xFB, 0xFD, 0xFF, 0xEE, 0xEF, 0xF7, 0xF7, 0xFB,
0xF3, 0xFC, 0xF9, 0xFE, 0x7C, 0xFF, 0x1C, 0x7F, 0x80, 0x3F, 0xC0, 0x1E, 0xF0, 0x7F, 0xE1, 0xFF, 0xC7, 0xFF, 0x9F,
0xFE, 0x7F, 0xFD, 0xFF, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC1, 0xFF, 0x03,
0xC0, 0x1F, 0xE0, 0x7F, 0xF1, 0xFF, 0xF3, 0xF7, 0xEF, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0,
0x7F, 0xE0, 0xFF, 0xF7, 0xE7, 0xFF, 0xC7, 0xFF, 0x07, 0xF8, 0x00, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0x3F, 0xF0, 0xFF,
0x0F, 0xF3, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0x1F, 0xE0, 0x7F, 0xF1,
0xFF, 0xE3, 0xF7, 0xEF, 0x83, 0xDE, 0x07, 0xFC, 0x07, 0xF8, 0x0F, 0xF0, 0x1F, 0xE0, 0x7F, 0xE0, 0xFF, 0xF7, 0xE7,
0xFF, 0xC7, 0xFF, 0x07, 0xF8, 0x01, 0xFC, 0x01, 0xFC, 0x01, 0xF8, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xE7, 0xEF, 0x0F,
0x78, 0x7B, 0xCF, 0xDF, 0xFE, 0xFF, 0xE7, 0xFE, 0x3D, 0xF1, 0xE7, 0xCF, 0x1F, 0x78, 0xFB, 0xC3, 0xE0, 0x1F, 0xE3,
0xFE, 0x7F, 0xE7, 0xDC, 0x78, 0x07, 0xE0, 0x7F, 0x83, 0xFE, 0x0F, 0xE0, 0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE,
0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03,
0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFE, 0x3F, 0xFB, 0xE7, 0xFF, 0x3F, 0xF0, 0x7F, 0x00, 0xF0, 0x1F, 0xF0, 0x3D, 0xE0,
0xFB, 0xC1, 0xE7, 0xC3, 0xC7, 0x8F, 0x8F, 0x1E, 0x1F, 0x3C, 0x1E, 0xF0, 0x3D, 0xE0, 0x3F, 0xC0, 0x7F, 0x00, 0xFE,
0x00, 0xF8, 0x01, 0xF0, 0x00, 0xF0, 0x00, 0xFF, 0x80, 0x1F, 0x78, 0xF1, 0xE7, 0x8F, 0x1E, 0x78, 0xF9, 0xE7, 0x9F,
0x9E, 0x7D, 0xFB, 0xC3, 0xDF, 0xFC, 0x3F, 0xFF, 0xC3, 0xFB, 0xFC, 0x3F, 0x9F, 0x81, 0xF9, 0xF8, 0x1F, 0x1F, 0x81,
0xF0, 0xF8, 0x0F, 0x0F, 0x00, 0xF8, 0x7D, 0xF3, 0xE7, 0xEF, 0x8F, 0xFC, 0x1F, 0xE0, 0x7F, 0x80, 0xFC, 0x01, 0xF0,
0x0F, 0xC0, 0x7F, 0x83, 0xFF, 0x0F, 0xFC, 0x7C, 0xF9, 0xE1, 0xFF, 0x87, 0xC0, 0xF8, 0x7F, 0xE1, 0xE7, 0xCF, 0x9F,
0x3C, 0x3F, 0xF0, 0x7F, 0x81, 0xFE, 0x03, 0xF0, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00,
0x78, 0x00, 0x7F, 0xF3, 0xFF, 0x9F, 0xFC, 0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x3E, 0x01, 0xE0, 0x1F, 0x01, 0xF0, 0x0F,
0x00, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F,
0x1E, 0x3C, 0x78, 0xF1, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0x01, 0xE0, 0x3C, 0x03, 0xC0, 0x78, 0x0F, 0x00, 0xF0, 0x1E,
0x03, 0xC0, 0x3C, 0x07, 0x80, 0xF8, 0x0F, 0x01, 0xE0, 0x3E, 0x03, 0xC0, 0x78, 0x0F, 0x80, 0xF0, 0xFF, 0xFF, 0xCF,
0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xFF, 0xFF, 0x0F, 0x01, 0xF8, 0x1F, 0x83, 0xFC, 0x3F,
0xC7, 0x9E, 0x79, 0xEF, 0x0F, 0x70, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x63, 0xCF, 0x9F, 0x18, 0x7F, 0x8F, 0xF8,
0xFF, 0x19, 0xF3, 0xFE, 0xFF, 0xFE, 0x7F, 0xCF, 0xFF, 0xEF, 0xFC, 0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F,
0x01, 0xFF, 0x3F, 0xF7, 0xFF, 0xFB, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0x7F, 0xFF, 0xBF, 0xF7, 0xFC, 0x1F, 0xCF, 0xF7,
0xFD, 0xF6, 0x78, 0x1E, 0x07, 0x81, 0xF3, 0x7F, 0xCF, 0xF1, 0xFC, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF1,
0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7C, 0xF7, 0xFF, 0x3F, 0xF1, 0xFF, 0x1F, 0xC3, 0xFE,
0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x3F, 0x3F, 0xBF, 0xDF, 0x6F,
0x07, 0xFB, 0xFD, 0xFE, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0x1F, 0xE7, 0xFD, 0xFF, 0xBE, 0xF7,
0x9E, 0xF3, 0xDF, 0x7B, 0xFF, 0x7F, 0xE7, 0xFC, 0x07, 0x9D, 0xF7, 0xFE, 0xFF, 0x9F, 0xE0, 0xF0, 0x3C, 0x0F, 0x03,
0xC0, 0xF0, 0x3F, 0xEF, 0xFF, 0xFF, 0xF7, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xFF, 0xFF, 0x0F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1E, 0x3C, 0x78, 0xF0, 0x03, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F,
0x3F, 0xFF, 0xF7, 0xE0, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE7, 0xFD, 0xF7, 0xFC, 0xFF, 0x1F, 0xC3, 0xFC,
0x7F, 0xCF, 0x79, 0xEF, 0xBC, 0xFF, 0x8F, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xFB, 0xFF, 0xDE,
0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xF7, 0xFF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3, 0xCF, 0xF3,
0xCF, 0xF3, 0xCF, 0xFF, 0xBF, 0xFF, 0xFF, 0xDF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x1F, 0xC3,
0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0xFF, 0x9F, 0xFB, 0xFF,
0x7B, 0xFF, 0x1F, 0xE3, 0xFC, 0x7F, 0xDF, 0xFF, 0xFF, 0xFB, 0xFE, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x1F, 0xF3,
0xFF, 0x7F, 0xF7, 0xCF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFF, 0x1F, 0xF0, 0x0F, 0x00, 0xF0, 0x0F,
0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xF2, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x3F, 0x9F, 0xE7, 0xF9, 0xEE, 0x7F,
0x9F, 0xE3, 0xFD, 0xDF, 0x7F, 0xDF, 0xE7, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xFB,
0xFF, 0x7F, 0x7F, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xEF, 0xFF, 0xDF, 0xF3, 0xFC, 0xF0, 0xFF,
0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x00, 0xF1, 0xCF, 0xF9, 0xE7,
0xBE, 0xF3, 0xCF, 0x7D, 0xE7, 0xBF, 0xE3, 0xFF, 0xF0, 0xFF, 0xF8, 0x7E, 0xF8, 0x3F, 0x7C, 0x0F, 0x3E, 0x07, 0x8E,
0x00, 0xF9, 0xE7, 0xBE, 0x7F, 0xC3, 0xF8, 0x1F, 0x81, 0xF0, 0x1F, 0x83, 0xFC, 0x7F, 0xC7, 0x9E, 0xF1, 0xF0, 0xF1,
0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F,
0xC0, 0xF8, 0x00, 0x7F, 0xDF, 0xF7, 0xFC, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xF0, 0xFF, 0xFF, 0xFF, 0xFC, 0x1F, 0x1F,
0x3F, 0x3E, 0x3C, 0x3C, 0x3C, 0x3C, 0x7C, 0xF8, 0xF8, 0xFC, 0x7C, 0x3C, 0x3C, 0x3C, 0x3E, 0x3F, 0x3F, 0x1F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8, 0xFC, 0x7C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3E, 0x1F,
0x1F, 0x3F, 0x3E, 0x3C, 0x3C, 0x3C, 0x7C, 0xFC, 0xF8, 0xF0, 0x3C, 0x77, 0xFF, 0xFF, 0xEF, 0xFE, 0xE3, 0xC0, 0xFF,
0xFF, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x0F, 0x03, 0xC0, 0xF0, 0x7F, 0x3F, 0xDF, 0xF7, 0xD9, 0xE0, 0x78, 0x1F,
0x37, 0xFC, 0xFF, 0x1F, 0xC3, 0xC0, 0xF0, 0x3C, 0x0F, 0xE3, 0xFC, 0xFF, 0x1F, 0x63, 0xC0, 0x78, 0x3F, 0xE7, 0xFC,
0xFF, 0x87, 0x80, 0xF0, 0x1E, 0x03, 0xFE, 0xFF, 0xDF, 0xF8, 0x00, 0x07, 0x0E, 0xFF, 0xF7, 0xFE, 0x7F, 0xC7, 0x9E,
0x79, 0xE7, 0xFC, 0x7F, 0xEF, 0xFF, 0x70, 0xE0, 0x78, 0x79, 0xF3, 0xE3, 0xCF, 0x0F, 0xFC, 0x1F, 0xE0, 0x7F, 0x80,
0xFC, 0x0F, 0xFC, 0x3F, 0xF0, 0x1E, 0x00, 0x78, 0x0F, 0xFC, 0x3F, 0xF0, 0x1E, 0x00, 0x78, 0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xC7, 0xF9, 0xFF, 0x3C, 0xE7, 0xC0, 0xFE, 0x1F, 0xE3, 0xFE, 0xF7,
0xDE, 0x3F, 0xE7, 0x3F, 0xE3, 0xF8, 0x3F, 0x80, 0xF3, 0x9E, 0x7F, 0xDF, 0xFB, 0xFE, 0x00, 0x77, 0xFB, 0xDD, 0xE0,
0x0F, 0xE0, 0x7F, 0xF1, 0xFB, 0xE3, 0x81, 0xEE, 0x7D, 0xFD, 0xF9, 0xFB, 0xB3, 0xFF, 0x07, 0xEE, 0xCF, 0xDF, 0x9F,
0x9F, 0xFB, 0x81, 0xE7, 0xEF, 0xC7, 0xFF, 0x03, 0xF8, 0x00, 0x7E, 0x7F, 0x6F, 0x7F, 0xFF, 0xFF, 0xFF, 0x7F, 0x00,
0x01, 0xC7, 0x3C, 0xF7, 0xDF, 0x7B, 0xEF, 0xBC, 0x7B, 0xE7, 0xDF, 0x3C, 0xF1, 0xC7, 0x00, 0x00, 0xFF, 0xFF, 0xFF,
0xFC, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0xFF, 0xFF, 0xF8, 0x0F, 0xE0, 0x7F, 0xF1, 0xFB, 0xE3, 0x81, 0xEE, 0xFD,
0xFD, 0xFD, 0xFB, 0xFB, 0xF7, 0xE7, 0xEF, 0xCF, 0xDF, 0xDF, 0xBB, 0xFB, 0x81, 0xE7, 0xEF, 0xC7, 0xFF, 0x03, 0xF8,
0x00, 0xFF, 0xFF, 0x7C, 0xFE, 0xFF, 0xFF, 0xFE, 0x7C, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78,
0x1E, 0x07, 0x81, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFC, 0x7D, 0xFD, 0xF8, 0xF3, 0xCF, 0x9E, 0x7F, 0xFE, 0xFD, 0xFD,
0xF9, 0xE3, 0xE3, 0xF3, 0xFF, 0xFC, 0x18, 0xF7, 0xFC, 0xE0, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F,
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x00, 0x0F, 0xF8, 0xFF, 0xF7, 0xFF, 0xDF, 0xEF, 0xFF, 0xBF,
0xFE, 0xF7, 0xFB, 0xDF, 0xEF, 0x3F, 0xBC, 0x7E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF,
0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC0, 0x77, 0xFE, 0xE0, 0x18, 0xE3, 0xDF, 0x7D, 0xE0, 0x1D, 0xFF, 0xDF, 0x1C, 0x71,
0xC7, 0x1C, 0x3F, 0x3F, 0xFF, 0xFE, 0x7F, 0x3F, 0xFD, 0xFE, 0x7E, 0x00, 0x03, 0x9E, 0x3E, 0xF0, 0xFB, 0xC3, 0xDF,
0x1F, 0x78, 0xF7, 0xCF, 0xBC, 0xFB, 0xC3, 0x9E, 0x00, 0x00, 0x3C, 0x1C, 0x1F, 0x87, 0x83, 0xF0, 0xF0, 0x7E, 0x3C,
0x01, 0xC7, 0x80, 0x39, 0xE0, 0x07, 0x3C, 0xF0, 0xEF, 0x3E, 0x1D, 0xEF, 0xC0, 0x7B, 0xB8, 0x0F, 0x7F, 0x83, 0xCF,
0xF0, 0x78, 0x1C, 0x1E, 0x03, 0x83, 0xC0, 0x00, 0x3C, 0x3C, 0x3F, 0x0F, 0x0F, 0xC7, 0x83, 0xF1, 0xE0, 0x1C, 0xF0,
0x07, 0x3C, 0x01, 0xDF, 0xF8, 0x77, 0xFF, 0x1F, 0xDF, 0xC0, 0xF0, 0xF0, 0x78, 0x78, 0x1C, 0x3C, 0x0F, 0x0E, 0x03,
0x87, 0xF1, 0xE1, 0xFC, 0xFC, 0x1E, 0x1F, 0xC3, 0xC1, 0xF8, 0xF0, 0x1F, 0x1E, 0x03, 0xE7, 0x80, 0x1C, 0xF0, 0x13,
0xBC, 0xF7, 0xF7, 0xBE, 0xFD, 0xEF, 0xC0, 0x3F, 0xB8, 0x0F, 0x7F, 0x81, 0xEF, 0xF0, 0x78, 0x1C, 0x0E, 0x03, 0x83,
0xC0, 0x00, 0x1E, 0x07, 0xC1, 0xF0, 0x78, 0x00, 0x03, 0x81, 0xE0, 0xF8, 0x7C, 0x1E, 0x0F, 0x03, 0xEE, 0xFF, 0x9F,
0xE3, 0xF8, 0x03, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xE0, 0x0F, 0xE0,
0x1F, 0xC0, 0x3F, 0xC0, 0xF7, 0x81, 0xEF, 0x07, 0xCF, 0x0F, 0x1E, 0x1F, 0xFC, 0x7F, 0xFC, 0xFF, 0xF9, 0xE0, 0xFF,
0xC1, 0xFF, 0x01, 0xE0, 0x01, 0x80, 0x07, 0x80, 0x0F, 0x00, 0x3C, 0x00, 0x70, 0x00, 0x00, 0x01, 0xF0, 0x03, 0xE0,
0x0F, 0xE0, 0x1F, 0xC0, 0x3F, 0xC0, 0xF7, 0x81, 0xEF, 0x07, 0xCF, 0x0F, 0x1E, 0x1F, 0xFC, 0x7F, 0xFC, 0xFF, 0xF9,
0xE0, 0xFF, 0xC1, 0xFF, 0x01, 0xE0, 0x03, 0x80, 0x0F, 0x80, 0x3F, 0x80, 0x7F, 0x00, 0x6E, 0x00, 0x00, 0x01, 0xF0,
0x03, 0xE0, 0x0F, 0xE0, 0x1F, 0xC0, 0x3F, 0xC0, 0xF7, 0x81, 0xEF, 0x07, 0xCF, 0x0F, 0x1E, 0x1F, 0xFC, 0x7F, 0xFC,
0xFF, 0xF9, 0xE0, 0xFF, 0xC1, 0xFF, 0x01, 0xE0, 0x0F, 0xF0, 0x3F, 0xE0, 0x37, 0x80, 0x00, 0x00, 0x7C, 0x00, 0xF8,
0x03, 0xF8, 0x07, 0xF0, 0x0F, 0xF0, 0x3D, 0xE0, 0x7B, 0xC1, 0xF3, 0xC3, 0xC7, 0x87, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE,
0x78, 0x3F, 0xF0, 0x7F, 0xC0, 0x78, 0x1E, 0xF0, 0x3D, 0xE0, 0x7B, 0xC0, 0x00, 0x00, 0x7C, 0x00, 0xF8, 0x03, 0xF8,
0x07, 0xF0, 0x0F, 0xF0, 0x3D, 0xE0, 0x7B, 0xC1, 0xF3, 0xC3, 0xC7, 0x87, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, 0x78, 0x3F,
0xF0, 0x7F, 0xC0, 0x78, 0x07, 0xC0, 0x0D, 0x80, 0x1F, 0x00, 0x3E, 0x00, 0x7C, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xF0,
0x1F, 0xF0, 0x3D, 0xE0, 0x7B, 0xC1, 0xF3, 0xC3, 0xC7, 0x87, 0xFF, 0x1F, 0xFF, 0x3F, 0xFE, 0x78, 0x3F, 0xF0, 0x7F,
0xC0, 0x78, 0x00, 0xFF, 0xF0, 0x07, 0xFF, 0x80, 0x7F, 0xFC, 0x07, 0xF8, 0x00, 0x3F, 0xC0, 0x03, 0xDE, 0x00, 0x1E,
0xFF, 0x81, 0xE7, 0xFC, 0x1F, 0x3F, 0xE0, 0xFF, 0xE0, 0x0F, 0xFF, 0x00, 0x7F, 0xF8, 0x07, 0xC3, 0xFF, 0xBC, 0x1F,
0xFF, 0xE0, 0xFF, 0xE0, 0x1F, 0xF1, 0xFF, 0x9F, 0xFC, 0xFD, 0xEF, 0x80, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07,
0x80, 0x3E, 0x01, 0xFC, 0xE7, 0xFF, 0x1F, 0xF8, 0x7F, 0xC0, 0xF0, 0x07, 0x80, 0x5C, 0x03, 0xE0, 0x1F, 0x00, 0x0C,
0x03, 0xC0, 0x78, 0x07, 0x80, 0x70, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0xFD, 0xFF, 0xBF,
0xF7, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x06, 0x00, 0xE0, 0x3C, 0x0F, 0x01, 0xC0, 0x00, 0x3F, 0xFF,
0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0xFD, 0xFF, 0xBF, 0xF7, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0xFF, 0xFF, 0xFE,
0x0E, 0x01, 0xE0, 0x7E, 0x1F, 0xC1, 0x98, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0xFD, 0xFF,
0xBF, 0xF7, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x7B, 0xCF, 0x79, 0xEF, 0x00, 0x0F, 0xFF, 0xFF, 0xFF,
0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0x7F, 0xEF, 0xFD, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x31,
0xC7, 0x8F, 0x1C, 0x07, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x78, 0x33, 0xDF, 0xEE, 0x03,
0xDE, 0xF7, 0xBD, 0xEF, 0x7B, 0xDE, 0xF7, 0xBD, 0xEF, 0x00, 0x30, 0xF3, 0xF7, 0xFC, 0xC0, 0x1E, 0x3C, 0x78, 0xF1,
0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x80, 0xF7, 0x7B, 0xFD, 0xC0, 0x03, 0xC1, 0xE0, 0xF0, 0x78,
0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x00, 0x3F, 0xE0, 0xFF, 0xF1, 0xFF, 0xF3,
0xEF, 0xE7, 0xC3, 0xEF, 0x87, 0xFF, 0xC7, 0xFF, 0x8F, 0x7C, 0x1E, 0xF8, 0x7D, 0xF0, 0xFB, 0xEF, 0xE7, 0xFF, 0xCF,
0xFF, 0x0F, 0xF8, 0x00, 0x0F, 0xE0, 0x7F, 0x81, 0xFC, 0x00, 0x00, 0xF0, 0x7F, 0xE1, 0xFF, 0xC7, 0xFF, 0x9F, 0xFE,
0x7F, 0xFD, 0xFF, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC1, 0xFF, 0x03, 0xC0,
0x03, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x00, 0x07, 0xF8, 0x1F, 0xFC, 0x7F, 0xFC, 0xFD, 0xFB,
0xE0, 0xF7, 0x81, 0xFF, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x1F, 0xF8, 0x3F, 0xFD, 0xF9, 0xFF, 0xF1, 0xFF, 0xC1,
0xFE, 0x00, 0x01, 0x80, 0x07, 0x80, 0x1F, 0x00, 0x3C, 0x00, 0x70, 0x00, 0x00, 0x07, 0xF8, 0x1F, 0xFC, 0x7F, 0xFC,
0xFD, 0xFB, 0xE0, 0xF7, 0x81, 0xFF, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x1F, 0xF8, 0x3F, 0xFD, 0xF9, 0xFF, 0xF1,
0xFF, 0xC1, 0xFE, 0x00, 0x03, 0x00, 0x0F, 0x00, 0x3F, 0x00, 0x7F, 0x00, 0xCC, 0x00, 0x00, 0x07, 0xF8, 0x1F, 0xFC,
0x7F, 0xFC, 0xFD, 0xFB, 0xE0, 0xF7, 0x81, 0xFF, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x1F, 0xF8, 0x3F, 0xFD, 0xF9,
0xFF, 0xF1, 0xFF, 0xC1, 0xFE, 0x00, 0x0F, 0x60, 0x3F, 0xC0, 0x7F, 0x80, 0x00, 0x01, 0xFE, 0x07, 0xFF, 0x1F, 0xFF,
0x3F, 0x7E, 0xF8, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xFE, 0x0F, 0xFF, 0x7E, 0x7F, 0xFC,
0x7F, 0xF0, 0x7F, 0x80, 0x1E, 0xE0, 0x3D, 0xE0, 0x7B, 0x80, 0x00, 0x01, 0xFE, 0x07, 0xFF, 0x1F, 0xFF, 0x3F, 0x7E,
0xF8, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x80, 0xFF, 0x01, 0xFE, 0x07, 0xFE, 0x0F, 0xFF, 0x7E, 0x7F, 0xFC, 0x7F, 0xF0,
0x7F, 0x80, 0x61, 0xBC, 0xFF, 0xFD, 0xFE, 0x3F, 0x1F, 0xEF, 0xFF, 0xCF, 0x61, 0x80, 0x00, 0x08, 0x3F, 0xF8, 0xFF,
0xF3, 0xFF, 0xC7, 0xEF, 0xDF, 0x1F, 0xBC, 0x7F, 0xF9, 0xEF, 0xF7, 0x9F, 0xEE, 0x3F, 0xFC, 0xFF, 0xF1, 0xFF, 0xEF,
0xCF, 0xFF, 0x9F, 0xFE, 0x7F, 0xF0, 0x60, 0x00, 0x0C, 0x00, 0xF0, 0x07, 0xC0, 0x1E, 0x00, 0x30, 0x00, 0x03, 0xC1,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFF, 0xEF, 0x9F,
0xFC, 0xFF, 0xC1, 0xFC, 0x00, 0x03, 0x00, 0x1C, 0x01, 0xE0, 0x1E, 0x00, 0xE0, 0x00, 0x03, 0xC1, 0xFE, 0x0F, 0xF0,
0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFF, 0xEF, 0x9F, 0xFC, 0xFF, 0xC1,
0xFC, 0x00, 0x07, 0x00, 0x78, 0x07, 0xE0, 0x3F, 0x81, 0x98, 0x00, 0x03, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC,
0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFF, 0xEF, 0x9F, 0xFC, 0xFF, 0xC1, 0xFC, 0x00, 0x3D,
0xE1, 0xEF, 0x0F, 0x78, 0x00, 0x0F, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF,
0x07, 0xF8, 0x3F, 0xE3, 0xFF, 0xBE, 0x7F, 0xF3, 0xFF, 0x07, 0xF0, 0x01, 0x80, 0x0F, 0x00, 0x3C, 0x01, 0xE0, 0x07,
0x00, 0x00, 0x0F, 0x87, 0xFE, 0x1E, 0x7C, 0xF9, 0xF3, 0xC3, 0xFF, 0x07, 0xF8, 0x1F, 0xE0, 0x3F, 0x00, 0xFC, 0x01,
0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0xF0, 0x0F, 0x00, 0xFF, 0xCF, 0xFE, 0xFF, 0xFF, 0x3F,
0xF0, 0xFF, 0x0F, 0xF3, 0xFF, 0xFF, 0xFF, 0xEF, 0xFC, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0x3F, 0x83, 0xFE, 0x1F, 0xF1,
0xF7, 0xCF, 0x1C, 0x79, 0xE3, 0xDE, 0x1E, 0xF0, 0xF7, 0x87, 0xBF, 0x3C, 0xFD, 0xE1, 0xEF, 0x6F, 0x7B, 0xFB, 0xFF,
0xDF, 0xFC, 0x0C, 0x03, 0xC0, 0x78, 0x07, 0x80, 0x70, 0x00, 0x1F, 0xE3, 0xFE, 0x3F, 0xC6, 0x7C, 0xFF, 0xBF, 0xFF,
0x9F, 0xF3, 0xFF, 0xFB, 0xFF, 0x3F, 0xE0, 0x07, 0x00, 0xE0, 0x3C, 0x0F, 0x01, 0xC0, 0x00, 0x1F, 0xE3, 0xFE, 0x3F,
0xC6, 0x7C, 0xFF, 0xBF, 0xFF, 0x9F, 0xF3, 0xFF, 0xFB, 0xFF, 0x3F, 0xE0, 0x0E, 0x03, 0xE0, 0xFE, 0x1F, 0xC3, 0xB0,
0x00, 0x1F, 0xE3, 0xFE, 0x3F, 0xC6, 0x7C, 0xFF, 0xBF, 0xFF, 0x9F, 0xF3, 0xFF, 0xFB, 0xFF, 0x3F, 0xE0, 0x3F, 0xC7,
0xF8, 0xDE, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0xF1, 0xFE, 0x33, 0xE7, 0xFD, 0xFF, 0xFC, 0xFF, 0x9F, 0xFF, 0xDF, 0xF9,
0xFF, 0x7B, 0xCF, 0x79, 0xEF, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0xF1, 0xFE, 0x33, 0xE7, 0xFD, 0xFF, 0xFC, 0xFF, 0x9F,
0xFF, 0xDF, 0xF9, 0xFF, 0x0F, 0x03, 0xE0, 0x7E, 0x0F, 0xC1, 0xF0, 0x1E, 0x00, 0x03, 0xFC, 0x7F, 0xC7, 0xF8, 0xCF,
0x9F, 0xF7, 0xFF, 0xF3, 0xFE, 0x7F, 0xFF, 0x7F, 0xE7, 0xFC, 0x7F, 0xFF, 0x1F, 0xFF, 0xE3, 0xFF, 0xF8, 0xCF, 0xDF,
0x3F, 0xFF, 0xDF, 0xFF, 0xFF, 0x9E, 0x03, 0xE7, 0xEE, 0xFF, 0xFF, 0x9F, 0xFF, 0xE3, 0xFF, 0xF8, 0x1F, 0xCF, 0xF7,
0xFD, 0xF6, 0x78, 0x1E, 0x07, 0x81, 0xF3, 0x7F, 0xCF, 0xF0, 0xFC, 0x3C, 0x0F, 0x07, 0xC1, 0xF0, 0x0C, 0x01, 0xE0,
0x1F, 0x00, 0x78, 0x03, 0x80, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F,
0xE3, 0xFE, 0x1F, 0xE0, 0x03, 0x00, 0x78, 0x0F, 0x80, 0xF0, 0x0E, 0x00, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF,
0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x06, 0x00, 0xF0, 0x1F, 0x81, 0xFC, 0x19, 0x80,
0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x3D,
0xC3, 0xDE, 0x3D, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFE, 0x7D, 0xF7, 0xFF, 0x7F, 0xF7, 0x80, 0x7E, 0xE7,
0xFE, 0x3F, 0xE1, 0xFE, 0x63, 0xCF, 0x8F, 0x18, 0x07, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x78, 0x18, 0xF7,
0xDE, 0x70, 0x07, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x78, 0x18, 0x3C, 0x7E, 0x7F, 0x66, 0x00, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x73, 0xBF, 0xDC, 0xE0, 0x00, 0x00, 0xF0, 0x78, 0x3C, 0x1E,
0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x07, 0x10, 0x7F, 0x81, 0xFC, 0x1F, 0xC0, 0x7E, 0x00, 0x78, 0x7F,
0xC7, 0xFE, 0x7F, 0xF3, 0xEF, 0x9E, 0x3C, 0xF1, 0xE7, 0xDF, 0x3F, 0xF0, 0xFF, 0x83, 0xF0, 0x3D, 0x9F, 0xE7, 0xF8,
0x00, 0x00, 0x3F, 0xEF, 0xFF, 0xFF, 0xF7, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0x06, 0x00, 0xE0,
0x0F, 0x00, 0x78, 0x03, 0x80, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F,
0xF3, 0xFE, 0x1F, 0xC0, 0x03, 0x00, 0x78, 0x07, 0x80, 0xF0, 0x0E, 0x00, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF,
0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0x06, 0x00, 0xF0, 0x1F, 0x81, 0xFC, 0x19, 0x80,
0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0x1E,
0xC3, 0xFC, 0x3F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFF, 0x7D, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7D, 0xF7,
0xFF, 0x3F, 0xE1, 0xFC, 0x3D, 0xC3, 0xDE, 0x3D, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFF, 0x7D, 0xF7, 0x8F,
0x78, 0xF7, 0x8F, 0x7D, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x00, 0x3F, 0xFF, 0xFF, 0xFF,
0x00, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x00, 0x00, 0x61, 0xFF, 0x3F, 0xE7, 0xFF, 0x7F, 0xF7, 0xFF, 0x77, 0xF7, 0xEF,
0x7F, 0xF7, 0xFF, 0x7F, 0xE7, 0xFC, 0x20, 0x00, 0x18, 0x0F, 0x03, 0xC0, 0x78, 0x0E, 0x00, 0x0F, 0x3F, 0xCF, 0xF3,
0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFE, 0xFF, 0xFD, 0xFF, 0x3F, 0xC0, 0x06, 0x03, 0xC1, 0xF0, 0x78, 0x1C, 0x00, 0x0F,
0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFE, 0xFF, 0xFD, 0xFF, 0x3F, 0xC0, 0x0C, 0x07, 0x83, 0xF0, 0xFE,
0x33, 0x00, 0x0F, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFE, 0xFF, 0xFD, 0xFF, 0x3F, 0xC0, 0x7B, 0x9F,
0xE7, 0xB8, 0x00, 0x00, 0x3C, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xFB, 0xFF, 0xF7, 0xFC, 0xFF, 0x03,
0x00, 0x78, 0x07, 0x80, 0xF0, 0x0E, 0x00, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3,
0xF8, 0x1F, 0x81, 0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01,
0xFF, 0x3F, 0xF7, 0xFE, 0xF7, 0xFE, 0x3F, 0xC7, 0xF8, 0xFF, 0xBF, 0xFF, 0xFF, 0xF7, 0xFC, 0xF0, 0x1E, 0x03, 0xC0,
0x78, 0x00, 0x3B, 0xC7, 0xBC, 0x3B, 0xC0, 0x00, 0x00, 0x0F, 0x1F, 0xF1, 0xE7, 0x9E, 0x79, 0xE7, 0xBC, 0x3F, 0xC3,
0xFC, 0x3F, 0x81, 0xF8, 0x1F, 0x00, 0xF0, 0xDF, 0x0F, 0xE0, 0xFC, 0x0F, 0x80, 0x0F, 0xE0, 0x1F, 0xC0, 0x00, 0x00,
0x3E, 0x00, 0x7C, 0x01, 0xFC, 0x03, 0xF8, 0x07, 0xF8, 0x1E, 0xF0, 0x3D, 0xE0, 0xF9, 0xE1, 0xE3, 0xC3, 0xFF, 0x8F,
0xFF, 0x9F, 0xFF, 0x3C, 0x1F, 0xF8, 0x3F, 0xE0, 0x3C, 0x3F, 0xC7, 0xF8, 0x00, 0x00, 0x07, 0xF8, 0xFF, 0x8F, 0xF1,
0x9F, 0x3F, 0xEF, 0xFF, 0xE7, 0xFC, 0xFF, 0xFE, 0xFF, 0xCF, 0xF8, 0x0F, 0xF0, 0x1F, 0xC0, 0x1F, 0x80, 0x00, 0x00,
0x7C, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xF0, 0x0F, 0xF0, 0x3D, 0xE0, 0x7B, 0xC1, 0xF3, 0xC3, 0xC7, 0x87, 0xFF, 0x1F,
0xFF, 0x3F, 0xFE, 0x78, 0x3F, 0xF0, 0x7F, 0xC0, 0x78, 0x3F, 0xC7, 0xF0, 0x7E, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0xF1,
0xFE, 0x33, 0xE7, 0xFD, 0xFF, 0xFC, 0xFF, 0x9F, 0xFF, 0xDF, 0xF9, 0xFF, 0x07, 0xC0, 0x07, 0xC0, 0x0F, 0xE0, 0x0F,
0xE0, 0x0F, 0xF0, 0x1E, 0xF0, 0x1E, 0xF0, 0x3E, 0x78, 0x3C, 0x78, 0x3F, 0xF8, 0x7F, 0xFC, 0x7F, 0xFC, 0x78, 0x3E,
0xF8, 0x3E, 0xF0, 0x1E, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x3C, 0x00, 0x3F, 0x00, 0x1F, 0x7F, 0x8F, 0xF8, 0xFF, 0x19,
0xF3, 0xFE, 0xFF, 0xFE, 0x7F, 0xCF, 0xFF, 0xEF, 0xFC, 0xFF, 0x81, 0xE0, 0x7C, 0x0F, 0xC0, 0xF8, 0x01, 0x80, 0x1E,
0x00, 0xF0, 0x0F, 0x00, 0x70, 0x00, 0x00, 0x7F, 0xC7, 0xFE, 0x7F, 0xF3, 0xF7, 0xBE, 0x01, 0xE0, 0x0F, 0x00, 0x78,
0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x07, 0xF3, 0x9F, 0xFC, 0x7F, 0xE1, 0xFF, 0x00, 0x03, 0x01, 0xE0, 0x78, 0x3C, 0x0E,
0x00, 0x01, 0xFC, 0xFF, 0x7F, 0xDF, 0x67, 0x81, 0xE0, 0x78, 0x1F, 0x37, 0xFC, 0xFF, 0x1F, 0xC0, 0x03, 0x00, 0x3C,
0x03, 0xF0, 0x1F, 0xC0, 0xCC, 0x00, 0x00, 0x7F, 0xC7, 0xFE, 0x7F, 0xF3, 0xF7, 0xBE, 0x01, 0xE0, 0x0F, 0x00, 0x78,
0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x07, 0xF3, 0x9F, 0xFC, 0x7F, 0xE1, 0xFF, 0x00, 0x06, 0x03, 0xC1, 0xF8, 0x7F, 0x19,
0x80, 0x01, 0xFC, 0xFF, 0x7F, 0xDF, 0x67, 0x81, 0xE0, 0x78, 0x1F, 0x37, 0xFC, 0xFF, 0x1F, 0xC0, 0x07, 0x80, 0x3C,
0x01, 0xE0, 0x00, 0x01, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xDE, 0xF8, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78,
0x03, 0xE0, 0x1F, 0xCE, 0x7F, 0xF1, 0xFF, 0x87, 0xFC, 0x0F, 0x03, 0xC0, 0xF0, 0x00, 0x00, 0x07, 0xF3, 0xFD, 0xFF,
0x7D, 0x9E, 0x07, 0x81, 0xE0, 0x7C, 0xDF, 0xF3, 0xFC, 0x7F, 0x0C, 0xC0, 0x7F, 0x03, 0xF0, 0x0F, 0x00, 0x30, 0x00,
0x00, 0x7F, 0xC7, 0xFE, 0x7F, 0xF3, 0xF7, 0xBE, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF8, 0x07,
0xF3, 0x9F, 0xFC, 0x7F, 0xE1, 0xFF, 0x00, 0x19, 0x87, 0xF1, 0xF8, 0x3C, 0x06, 0x00, 0x01, 0xFC, 0xFF, 0x7F, 0xDF,
0x67, 0x81, 0xE0, 0x78, 0x1F, 0x37, 0xFC, 0xFF, 0x1F, 0xC0, 0x1D, 0x80, 0x7F, 0x01, 0xFC, 0x03, 0xE0, 0x07, 0x00,
0x00, 0x0F, 0xFC, 0x3F, 0xFC, 0xFF, 0xF3, 0xCF, 0xEF, 0x07, 0xBC, 0x1F, 0xF0, 0x3F, 0xC0, 0xFF, 0x03, 0xFC, 0x1F,
0xF0, 0x7B, 0xCF, 0xEF, 0xFF, 0x3F, 0xF8, 0xFF, 0xC0, 0x00, 0xFF, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE, 0x00, 0xFE,
0x1F, 0xF0, 0x3F, 0xF0, 0x7F, 0xF0, 0x7D, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x78, 0xF0, 0x7C, 0xF0, 0x7F, 0xF0, 0x3F,
0xF0, 0x1F, 0xF0, 0x3F, 0xE0, 0xFF, 0xF1, 0xFF, 0xF3, 0xEF, 0xE7, 0xC3, 0xEF, 0x87, 0xFF, 0xC7, 0xFF, 0x8F, 0x7C,
0x1E, 0xF8, 0x7D, 0xF0, 0xFB, 0xEF, 0xE7, 0xFF, 0xCF, 0xFF, 0x0F, 0xF8, 0x00, 0x00, 0xF0, 0x3F, 0xC1, 0xFE, 0x01,
0xE0, 0x0F, 0x0F, 0xF8, 0xFF, 0xCF, 0xFE, 0x7D, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0xCF, 0x3F, 0xF8, 0xFF, 0xC3,
0xFE, 0x3F, 0x87, 0xF0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x80, 0xF0, 0x1F, 0xFB, 0xFF, 0x7F, 0xEF, 0x01,
0xE0, 0x3C, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xC3, 0xFC, 0x00, 0x00, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF,
0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x3F, 0xC7, 0xF0, 0x7E, 0x00, 0x0F, 0xFF, 0xFF,
0xFF, 0xFF, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0x7F, 0xEF, 0xFD, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x80,
0x3F, 0xC1, 0xFC, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFE, 0x7D, 0xF7, 0xFF, 0x7F, 0xF7, 0x80, 0x7E,
0xE7, 0xFE, 0x3F, 0xE1, 0xFE, 0x0F, 0x01, 0xE0, 0x3C, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xF0, 0x1E, 0x03,
0xFF, 0x7F, 0xEF, 0xFD, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x00,
0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFE, 0x7D, 0xF7, 0xFF, 0x7F, 0xF7, 0x80, 0x7E, 0xE7, 0xFE, 0x3F, 0xE1, 0xFE, 0xFF,
0xEF, 0xFE, 0xFF, 0xEF, 0x00, 0xF0, 0x0F, 0x00, 0xFF, 0xCF, 0xFC, 0xFF, 0xCF, 0x00, 0xF0, 0x0F, 0x00, 0xFF, 0xEF,
0xFE, 0xFF, 0xE0, 0x1E, 0x01, 0xC0, 0x3C, 0x03, 0xF0, 0x1F, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7, 0xDF, 0x7F, 0xF7, 0xFF,
0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x3C, 0x03, 0x80, 0x7E, 0x03, 0xE0, 0x19, 0x87, 0xF0, 0x7E, 0x07,
0x80, 0x60, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xC0, 0x78, 0x0F, 0xFD, 0xFF, 0xBF, 0xF7, 0x80, 0xF0, 0x1E,
0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x19, 0x81, 0xFC, 0x1F, 0x80, 0xF0, 0x06, 0x00, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xE7,
0xDF, 0x7F, 0xF7, 0xFF, 0x78, 0x07, 0xEE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0x03, 0x00, 0x3C, 0x03, 0xF0, 0x1F, 0xC0,
0xCC, 0x00, 0x00, 0x7F, 0xC7, 0xFE, 0x7F, 0xF3, 0xF3, 0xBE, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x3F, 0xC1, 0xFE, 0x0F,
0xF8, 0x7F, 0xF3, 0xDF, 0xFE, 0x7F, 0xF1, 0xFF, 0x80, 0x03, 0x00, 0xF0, 0x3F, 0x0F, 0xE0, 0xCC, 0x00, 0x07, 0xF9,
0xFF, 0x7F, 0xEF, 0xBD, 0xE7, 0xBC, 0xF7, 0xDE, 0xFF, 0xDF, 0xF9, 0xFF, 0x01, 0xE7, 0x7D, 0xFF, 0xBF, 0xE7, 0xF8,
0x1F, 0xE0, 0x7F, 0x03, 0xF0, 0x00, 0x01, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xCE, 0xF8, 0x07, 0x80, 0x3C, 0x01, 0xE0,
0xFF, 0x07, 0xF8, 0x3F, 0xE1, 0xFF, 0xCF, 0x7F, 0xF9, 0xFF, 0xC7, 0xFE, 0x1F, 0xC3, 0xF8, 0x3E, 0x00, 0x00, 0x00,
0x3F, 0xCF, 0xFB, 0xFF, 0x7D, 0xEF, 0x3D, 0xE7, 0xBE, 0xF7, 0xFE, 0xFF, 0xCF, 0xF8, 0x0F, 0x3B, 0xEF, 0xFD, 0xFF,
0x3F, 0xC0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x00, 0x01, 0xFF, 0x1F, 0xF9, 0xFF, 0xCF, 0xCE, 0xF8, 0x07, 0x80, 0x3C,
0x01, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xE1, 0xFF, 0xCF, 0x7F, 0xF9, 0xFF, 0xC7, 0xFE, 0x07, 0x01, 0xE0, 0x1C, 0x00,
0x00, 0x00, 0x3F, 0xCF, 0xFB, 0xFF, 0x7D, 0xEF, 0x3D, 0xE7, 0xBE, 0xF7, 0xFE, 0xFF, 0xCF, 0xF8, 0x0F, 0x3B, 0xEF,
0xFD, 0xFF, 0x3F, 0xC0, 0x1F, 0xF1, 0xFF, 0x9F, 0xFC, 0xFC, 0xEF, 0x80, 0x78, 0x03, 0xC0, 0x1E, 0x0F, 0xF0, 0x7F,
0x83, 0xFE, 0x1F, 0xFC, 0xF7, 0xFF, 0x9F, 0xFC, 0x7F, 0xE0, 0x00, 0x03, 0x80, 0x3C, 0x03, 0xE0, 0x1E, 0x00, 0x03,
0x80, 0xF0, 0x1E, 0x03, 0x80, 0x00, 0x00, 0x07, 0xF9, 0xFF, 0x7F, 0xEF, 0xBD, 0xE7, 0xBC, 0xF7, 0xDE, 0xFF, 0xDF,
0xF9, 0xFF, 0x01, 0xE7, 0x7D, 0xFF, 0xBF, 0xE7, 0xF8, 0x03, 0x00, 0x3C, 0x03, 0xF0, 0x3F, 0x80, 0xCC, 0x00, 0x03,
0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x0F, 0xF0, 0x7F, 0x83,
0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0x80, 0x38, 0x3F, 0x8F, 0xE0, 0x00, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3F, 0xEF,
0xFF, 0xFF, 0xF7, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0x7C, 0x3C, 0x7C, 0x3C, 0xFF, 0xFF, 0xFF,
0xFF, 0x7C, 0x3C, 0x7C, 0x3C, 0x7F, 0xFC, 0x7F, 0xFC, 0x7F, 0xFC, 0x7C, 0x3C, 0x7C, 0x3C, 0x7C, 0x3C, 0x7C, 0x3C,
0x7C, 0x3C, 0x7C, 0x3C, 0x78, 0x07, 0x80, 0xFF, 0x0F, 0xF0, 0x78, 0x07, 0xFC, 0x7F, 0xE7, 0xFE, 0x7D, 0xE7, 0x8F,
0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7B, 0x7F, 0xBF, 0xC0, 0x03, 0xC1, 0xE0, 0xF0, 0x78, 0x3C,
0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x00, 0x7F, 0xFF, 0xFE, 0x00, 0x00, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x7F, 0x7F, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0xFF, 0xFF, 0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C, 0xEF, 0xFD, 0xF0, 0x07, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78,
0xF1, 0xE0, 0xFF, 0x7F, 0x7E, 0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x79,
0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x73, 0xCF, 0xDF, 0x79, 0xE7, 0x9E, 0x01, 0xE7,
0x9E, 0x79, 0xE7, 0x9E, 0x79, 0xE7, 0x9E, 0x7B, 0xCF, 0xBE, 0xF8, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0,
0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF3, 0xBE, 0xF3, 0xFE, 0xF7, 0xFE, 0xF3, 0xFC,
0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0x00, 0x3C, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F,
0xCF, 0x07, 0xC7, 0xF1, 0xF8, 0x7E, 0x01, 0x80, 0x3C, 0x07, 0xE0, 0xFE, 0x06, 0x60, 0x00, 0x03, 0xC0, 0x3C, 0x03,
0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC6, 0x7C, 0xFF, 0xCF, 0xF8, 0xFF, 0x00,
0x0C, 0x0F, 0x0F, 0xCF, 0xF3, 0x30, 0x00, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E,
0x1F, 0x3F, 0x9F, 0x8F, 0xC0, 0xF0, 0xFB, 0xC7, 0xCF, 0x3E, 0x3D, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0xE0, 0x3F, 0xC0,
0xFF, 0x83, 0xDF, 0x0F, 0x7C, 0x3C, 0xF8, 0xF1, 0xF3, 0xC3, 0xEF, 0x0F, 0x80, 0x00, 0x07, 0x00, 0x3C, 0x00, 0xF0,
0x03, 0xC0, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE7, 0xFD, 0xF7, 0xFC, 0xFF, 0x1F, 0xC3, 0xFC, 0x7F, 0xCF,
0x79, 0xEF, 0xBC, 0xFF, 0x8F, 0x00, 0x01, 0xC0, 0x78, 0x1F, 0x03, 0xC0, 0xF3, 0xEF, 0x7C, 0xFF, 0x8F, 0xF0, 0xFE,
0x0F, 0xF0, 0xFF, 0x0F, 0xF8, 0xF7, 0xCF, 0x3E, 0xF1, 0xE0, 0x18, 0x07, 0x81, 0xF0, 0x78, 0x0E, 0x00, 0x00, 0x3C,
0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xFF, 0xFF, 0xFF,
0xFE, 0x19, 0xEF, 0xF8, 0x03, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xEF, 0xFF, 0x78, 0xF0, 0x1E,
0x03, 0xC0, 0x78, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8,
0x00, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x00, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xF3, 0xCF, 0x3C, 0xFB, 0xFF, 0xDE,
0x01, 0xCF, 0x3C, 0xF0, 0xF0, 0x1E, 0x73, 0xCE, 0x79, 0xCF, 0x39, 0xE6, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0,
0x78, 0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0xF7, 0xF7, 0xF7, 0xF7, 0xF6, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF8,
0xFC, 0xFC, 0x78, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x3D, 0xE7, 0xBC, 0xF7, 0x9E, 0xF0, 0x1E, 0x03, 0xC0, 0x78,
0x0F, 0xFF, 0xFF, 0xFF, 0xF8, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3D, 0xEF, 0x7F, 0xDF, 0xF7, 0xBC, 0x0F, 0x03,
0xC0, 0xF8, 0x3F, 0x0F, 0xC1, 0xE0, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xF8, 0x1F, 0xC0, 0xFE, 0x1F, 0xC0,
0xFC, 0x07, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xFF, 0x1F, 0xF8, 0xFF, 0xC0, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xF0, 0xFC,
0xFE, 0xFC, 0x7E, 0x2F, 0x07, 0x83, 0xC1, 0xF0, 0xF8, 0x7C, 0x1E, 0x01, 0x80, 0x0F, 0x00, 0x7C, 0x03, 0xE0, 0x06,
0x00, 0x00, 0x0F, 0x07, 0xFE, 0x1F, 0xFC, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0xDF, 0xFF, 0xFF, 0xDF, 0xFF, 0x3F, 0xFC,
0x7F, 0xF1, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x1F, 0xF0, 0x3C, 0x06, 0x03, 0xC1, 0xF0, 0x78, 0x18, 0x00, 0x0F, 0xFB,
0xFF, 0xFF, 0xFD, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xC0, 0xF0, 0x7F, 0xE1, 0xFF, 0xC7, 0xFF,
0x9F, 0xFE, 0x7F, 0xFD, 0xFF, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC7, 0xFF, 0x1F, 0xFC, 0x3F, 0xF0, 0xFF, 0xC1, 0xFF,
0x03, 0xC0, 0x00, 0x03, 0x80, 0x1E, 0x00, 0xF8, 0x03, 0xC0, 0xFF, 0xBF, 0xFF, 0xFF, 0xDF, 0xF3, 0xFC, 0xFF, 0x3F,
0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x00, 0x0E, 0x07, 0x83, 0xE0, 0xF0, 0x0C, 0xC0, 0x7F, 0x00, 0xFC, 0x01, 0xE0, 0x03,
0x00, 0x00, 0x0F, 0x07, 0xFE, 0x1F, 0xFC, 0x7F, 0xF9, 0xFF, 0xE7, 0xFF, 0xDF, 0xFF, 0xFF, 0xDF, 0xFF, 0x3F, 0xFC,
0x7F, 0xF1, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x1F, 0xF0, 0x3C, 0x33, 0x0F, 0xE3, 0xF0, 0x78, 0x0C, 0x00, 0x0F, 0xFB,
0xFF, 0xFF, 0xFD, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xC0, 0x70, 0x03, 0x80, 0x1C, 0x01, 0xE0,
0x0E, 0x00, 0x0F, 0xF0, 0x7F, 0xC3, 0xFF, 0x1E, 0xF8, 0xF3, 0xC7, 0x9E, 0x3C, 0xF1, 0xE7, 0x8F, 0x3C, 0x79, 0xE3,
0xCF, 0xF0, 0x7F, 0xE1, 0xFF, 0xC7, 0xFF, 0x9F, 0xFE, 0x7F, 0xFD, 0xFF, 0xFF, 0xFD, 0xFF, 0xF3, 0xFF, 0xC7, 0xFF,
0x1F, 0xFC, 0x3F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xC0, 0x1F, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x00, 0xFF, 0xBF, 0xFF,
0xFF, 0xDF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3C, 0x0F, 0x03, 0xC3, 0xF0, 0xF8, 0x1F, 0xE0, 0x3F,
0xC0, 0x00, 0x00, 0xFF, 0x03, 0xFF, 0x8F, 0xFF, 0x9F, 0xBF, 0x7C, 0x1E, 0xF0, 0x3F, 0xE0, 0x3F, 0xC0, 0x7F, 0x80,
0xFF, 0x03, 0xFF, 0x07, 0xFF, 0xBF, 0x3F, 0xFE, 0x3F, 0xF8, 0x3F, 0xC0, 0x1F, 0xC1, 0xFC, 0x00, 0x00, 0x00, 0x1F,
0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7, 0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0x1F, 0xE0, 0x1F,
0xC0, 0x3F, 0x00, 0x00, 0x01, 0xFE, 0x07, 0xFF, 0x1F, 0xFF, 0x3F, 0x7E, 0xF8, 0x3D, 0xE0, 0x7F, 0xC0, 0x7F, 0x80,
0xFF, 0x01, 0xFE, 0x07, 0xFE, 0x0F, 0xFF, 0x7E, 0x7F, 0xFC, 0x7F, 0xF0, 0x7F, 0x80, 0x1D, 0xC1, 0xFC, 0x0F, 0x80,
0x00, 0x00, 0x01, 0xFC, 0x3F, 0xE7, 0xFF, 0x7D, 0xF7, 0x8F, 0x78, 0xF7, 0x8F, 0x7D, 0xF7, 0xFF, 0x3F, 0xE1, 0xFC,
0x03, 0x30, 0x0E, 0xF0, 0x3D, 0xC0, 0x77, 0x00, 0xCC, 0x00, 0x00, 0x07, 0xF8, 0x1F, 0xFC, 0x7F, 0xFC, 0xFD, 0xFB,
0xE0, 0xF7, 0x81, 0xFF, 0x01, 0xFE, 0x03, 0xFC, 0x07, 0xF8, 0x1F, 0xF8, 0x3F, 0xFD, 0xF9, 0xFF, 0xF1, 0xFF, 0xC1,
0xFE, 0x00, 0x0E, 0x60, 0xEF, 0x1F, 0xE1, 0xDC, 0x19, 0x80, 0x00, 0x1F, 0xC3, 0xFE, 0x7F, 0xF7, 0xDF, 0x78, 0xF7,
0x8F, 0x78, 0xF7, 0xDF, 0x7F, 0xF3, 0xFE, 0x1F, 0xC0, 0x0F, 0xFF, 0xE3, 0xFF, 0xFE, 0x7F, 0xFF, 0xEF, 0xE7, 0x80,
0xF8, 0x78, 0x0F, 0x07, 0x80, 0xF0, 0x7F, 0xCF, 0x07, 0xFC, 0xF0, 0x7F, 0xCF, 0x07, 0x80, 0xF8, 0x78, 0x0F, 0xE7,
0x80, 0x7F, 0xFF, 0xF3, 0xFF, 0xFF, 0x0F, 0xFF, 0xF0, 0x1F, 0x9F, 0x87, 0xFF, 0xF9, 0xFF, 0xFF, 0xBE, 0xFC, 0xF7,
0x8F, 0xFE, 0xF1, 0xFF, 0xDE, 0x3C, 0x03, 0xEF, 0xEE, 0x7F, 0xFF, 0xC7, 0xFF, 0xF8, 0x7E, 0x7F, 0x00, 0x03, 0x00,
0x3C, 0x03, 0xE0, 0x3C, 0x01, 0xC0, 0x00, 0x03, 0xFF, 0x1F, 0xFC, 0xFF, 0xF7, 0x9F, 0xBC, 0x3D, 0xE1, 0xEF, 0x3F,
0x7F, 0xFB, 0xFF, 0x9F, 0xF8, 0xF7, 0xC7, 0x9F, 0x3C, 0x7D, 0xE3, 0xEF, 0x0F, 0x80, 0x0C, 0x1E, 0x3E, 0x3C, 0x30,
0x00, 0xFF, 0xFF, 0xFF, 0xF2, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF, 0xC7, 0xFF, 0x3F, 0xFD, 0xE7, 0xEF,
0x0F, 0x78, 0x7B, 0xCF, 0xDF, 0xFE, 0xFF, 0xE7, 0xFE, 0x3D, 0xF1, 0xE7, 0xCF, 0x1F, 0x78, 0xFB, 0xC3, 0xE0, 0x00,
0x07, 0x00, 0x78, 0x07, 0xC0, 0x3C, 0x00, 0x7F, 0xBF, 0xDF, 0xEF, 0x27, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E,
0x00, 0x07, 0x83, 0xC3, 0xC1, 0xE0, 0x19, 0x81, 0xFC, 0x07, 0xE0, 0x1E, 0x00, 0x60, 0x7F, 0xE3, 0xFF, 0x9F, 0xFE,
0xF3, 0xF7, 0x87, 0xBC, 0x3D, 0xE7, 0xEF, 0xFF, 0x7F, 0xF3, 0xFF, 0x1E, 0xF8, 0xF3, 0xE7, 0x8F, 0xBC, 0x7D, 0xE1,
0xF0, 0x66, 0x7F, 0x7E, 0x3C, 0x18, 0x00, 0xFF, 0xFF, 0xFF, 0xF2, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0x03,
0x00, 0x38, 0x07, 0x80, 0xF0, 0x0E, 0x00, 0x00, 0x1F, 0xE3, 0xFE, 0x7F, 0xE7, 0xDC, 0x78, 0x07, 0xE0, 0x7F, 0x83,
0xFE, 0x0F, 0xE0, 0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0x06, 0x03, 0xC0, 0xF0, 0x78, 0x1C, 0x00,
0x03, 0xF9, 0xFE, 0x7F, 0x9E, 0xE7, 0xF9, 0xFE, 0x3F, 0xDD, 0xF7, 0xFD, 0xFE, 0x7F, 0x00, 0x06, 0x00, 0xF0, 0x1F,
0x81, 0xFC, 0x19, 0x80, 0x00, 0x1F, 0xE3, 0xFE, 0x7F, 0xE7, 0xDC, 0x78, 0x07, 0xE0, 0x7F, 0x83, 0xFE, 0x0F, 0xE0,
0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0x0E, 0x07, 0x83, 0xF0, 0xFE, 0x33, 0x00, 0x03, 0xF9, 0xFE,
0x7F, 0x9E, 0xE7, 0xF9, 0xFE, 0x3F, 0xDD, 0xF7, 0xFD, 0xFE, 0x7F, 0x00, 0x1F, 0xE3, 0xFE, 0x7F, 0xE7, 0xDC, 0x78,
0x07, 0xE0, 0x7F, 0x83, 0xFE, 0x0F, 0xE0, 0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0xE0, 0x0F, 0x01,
0x70, 0x1F, 0x01, 0xF0, 0x3F, 0x9F, 0xE7, 0xF9, 0xEE, 0x7F, 0x9F, 0xE3, 0xFD, 0xDF, 0x7F, 0xDF, 0xEF, 0xF0, 0x70,
0x1E, 0x0B, 0x83, 0xE0, 0xF8, 0x19, 0x81, 0xFC, 0x1F, 0x80, 0xF8, 0x07, 0x00, 0x00, 0x1F, 0xE3, 0xFE, 0x7F, 0xE7,
0xDC, 0x78, 0x07, 0xE0, 0x7F, 0x83, 0xFE, 0x0F, 0xE0, 0x3F, 0x01, 0xF7, 0xBF, 0x7F, 0xFF, 0xFE, 0x7F, 0xC0, 0x3B,
0x0F, 0xE3, 0xF8, 0x7C, 0x0E, 0x00, 0x03, 0xF9, 0xFE, 0x7F, 0x9E, 0xE7, 0xF9, 0xFE, 0x3F, 0xDD, 0xF7, 0xFD, 0xFE,
0x7F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03,
0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, 0xE0, 0x0F, 0x00, 0xFC, 0x07, 0xC0, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF,
0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xFB, 0xFF, 0x7F, 0x7F, 0x1C, 0x3E, 0x3E, 0x7C, 0x1D, 0x80, 0xFE, 0x07, 0xF0, 0x1F,
0x00, 0x70, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01,
0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x00, 0x0F, 0x0E, 0xFE, 0xFE, 0xF0, 0xFF, 0xFF, 0xFF, 0xF0,
0xF0, 0xF0, 0xF0, 0xFB, 0xFF, 0x7F, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x0F,
0xFC, 0x7F, 0xE0, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x00, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF,
0xF0, 0xFF, 0xFF, 0xF0, 0xFB, 0xFF, 0x7F, 0x7F, 0x1F, 0xC1, 0xFF, 0x07, 0xF0, 0x00, 0x0F, 0x07, 0xF8, 0x3F, 0xC1,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xE3, 0xFF, 0xBE, 0x7F, 0xF3, 0xFF, 0x07,
0xF0, 0x3F, 0x9F, 0xE7, 0xF8, 0x00, 0x00, 0x3C, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xFB, 0xFF, 0xF7,
0xFC, 0xFF, 0x1F, 0xC0, 0xFE, 0x00, 0x01, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC,
0x1F, 0xE0, 0xFF, 0x07, 0xFC, 0x7F, 0xF7, 0xCF, 0xFE, 0x7F, 0xE0, 0xFE, 0x00, 0x7F, 0x9F, 0xE0, 0x00, 0x00, 0xF3,
0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xEF, 0xFF, 0xDF, 0xF3, 0xFC, 0x1D, 0xC0, 0xFE, 0x03, 0xE0, 0x00,
0x0F, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xE3, 0xFF,
0xBE, 0x7F, 0xF3, 0xFF, 0x07, 0xF0, 0x7F, 0x8F, 0xE3, 0xF0, 0x00, 0x00, 0x3C, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF,
0x3F, 0xCF, 0xFB, 0xFF, 0xF7, 0xFC, 0xFF, 0x0F, 0x00, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0x83, 0xC1, 0xFE,
0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFF, 0xEF, 0x9F, 0xFC,
0xFF, 0xC1, 0xFC, 0x00, 0x1E, 0x07, 0xC3, 0xF0, 0xFC, 0x1F, 0x07, 0x80, 0x03, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF,
0xF3, 0xFC, 0xFF, 0xBF, 0xFF, 0x7F, 0xCF, 0xF0, 0x06, 0x70, 0x7F, 0x87, 0xB8, 0x3B, 0x81, 0x9C, 0x00, 0x03, 0xC1,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF8, 0xFF, 0xEF, 0x9F,
0xFC, 0xFF, 0xC1, 0xFC, 0x00, 0x1D, 0xC3, 0xB8, 0xEF, 0x3D, 0xC7, 0x30, 0x00, 0x3C, 0xF7, 0x9E, 0xF3, 0xDE, 0x7B,
0xCF, 0x79, 0xEF, 0x3D, 0xF7, 0xBF, 0xF3, 0xFE, 0x3F, 0xC0, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8,
0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFE, 0x3F, 0xFB, 0xE7, 0xFF, 0x3F, 0xF0, 0x7F, 0x00, 0xF0, 0x07, 0x00,
0x7E, 0x01, 0xF0, 0xF3, 0xFC, 0xFF, 0x3F, 0xCF, 0xF3, 0xFC, 0xFF, 0x3F, 0xEF, 0xFF, 0xDF, 0xF3, 0xFC, 0x1E, 0x07,
0xC1, 0xF0, 0x7C, 0x00, 0x60, 0x00, 0x0F, 0x00, 0x01, 0xF8, 0x00, 0x3F, 0xC0, 0x01, 0x98, 0x00, 0x00, 0x00, 0xF0,
0x00, 0xFF, 0x80, 0x1F, 0x78, 0xF1, 0xE7, 0x8F, 0x1E, 0x78, 0xF9, 0xE7, 0x9F, 0x9E, 0x7D, 0xFB, 0xC3, 0xDF, 0xFC,
0x3F, 0xFF, 0xC3, 0xFB, 0xFC, 0x3F, 0x9F, 0x81, 0xF9, 0xF8, 0x1F, 0x1F, 0x81, 0xF0, 0xF8, 0x0F, 0x0F, 0x00, 0x01,
0x80, 0x01, 0xE0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0x66, 0x00, 0x00, 0x03, 0xC7, 0x3F, 0xE7, 0x9E, 0xFB, 0xCF, 0x3D,
0xF7, 0x9E, 0xFF, 0x8F, 0xFF, 0xC3, 0xFF, 0xE1, 0xFB, 0xE0, 0xFD, 0xF0, 0x3C, 0xF8, 0x1E, 0x38, 0x00, 0x03, 0x00,
0x1E, 0x00, 0xFC, 0x07, 0xF8, 0x0C, 0xC0, 0x00, 0x0F, 0x87, 0xFE, 0x1E, 0x7C, 0xF9, 0xF3, 0xC3, 0xFF, 0x07, 0xF8,
0x1F, 0xE0, 0x3F, 0x00, 0xFC, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x06, 0x00, 0xF0,
0x1F, 0x83, 0xF8, 0x19, 0x80, 0x00, 0xF1, 0xFF, 0x1E, 0x79, 0xE7, 0x9E, 0x7B, 0xC3, 0xFC, 0x3F, 0xC3, 0xF8, 0x1F,
0x81, 0xF0, 0x0F, 0x0D, 0xF0, 0xFE, 0x0F, 0xC0, 0xF8, 0x00, 0x1C, 0xE0, 0x7F, 0x81, 0xCE, 0x00, 0x00, 0xF8, 0x7F,
0xE1, 0xE7, 0xCF, 0x9F, 0x3C, 0x3F, 0xF0, 0x7F, 0x81, 0xFE, 0x03, 0xF0, 0x0F, 0xC0, 0x1E, 0x00, 0x78, 0x01, 0xE0,
0x07, 0x80, 0x1E, 0x00, 0x78, 0x00, 0x03, 0x00, 0x1C, 0x01, 0xE0, 0x1E, 0x00, 0xE0, 0x00, 0x01, 0xFF, 0xCF, 0xFE,
0x7F, 0xF0, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x07, 0x80, 0x7C, 0x07, 0xC0, 0x3C, 0x03, 0xE0, 0x3F, 0xFF, 0xFF,
0xFF, 0xFF, 0x80, 0x06, 0x01, 0xC0, 0xF0, 0x78, 0x1C, 0x00, 0x07, 0xFD, 0xFF, 0x7F, 0xC1, 0xE0, 0xF0, 0x78, 0x3C,
0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0, 0x07, 0x00, 0x78, 0x01, 0xC0, 0x00, 0x07, 0xFF, 0x3F, 0xF9, 0xFF, 0xC0, 0x3E,
0x03, 0xE0, 0x3E, 0x03, 0xE0, 0x1E, 0x01, 0xF0, 0x1F, 0x00, 0xF0, 0x0F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1E,
0x07, 0x81, 0xE0, 0x00, 0x00, 0x1F, 0xF7, 0xFD, 0xFF, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x7C, 0x3F, 0xFF, 0xFF, 0xFF,
0x0D, 0xC0, 0xFE, 0x07, 0xF0, 0x1F, 0x00, 0x70, 0x00, 0x01, 0xFF, 0xCF, 0xFE, 0x7F, 0xF0, 0x0F, 0x80, 0xF8, 0x0F,
0x80, 0xF8, 0x07, 0x80, 0x7C, 0x07, 0xC0, 0x3C, 0x03, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x33, 0x0F, 0xE3,
0xF0, 0x7C, 0x0E, 0x00, 0x07, 0xFD, 0xFF, 0x7F, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1F, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
0x3F, 0x3F, 0xBF, 0xDF, 0x6F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0x7E,
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0x3B, 0xDE, 0xEF, 0x78, 0x7B, 0xDF, 0xEF, 0x70, 0x7B, 0xDF, 0xEF, 0x70, 0x3B, 0xDE, 0xE7,
0xF9, 0xDE, 0xF7, 0xBD, 0xE0, 0x7F, 0xBF, 0xDD, 0xFE, 0xFF, 0x77, 0x38, 0x7F, 0xBF, 0xDD, 0xFE, 0xFF, 0x77, 0x38,
0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x03, 0x80, 0xE0,
0x38, 0x0E, 0x03, 0x80, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0xFF, 0xFF, 0xFF, 0xFC, 0x78, 0x1E, 0x07, 0x8F, 0xFF, 0xFF,
0xFF, 0xC7, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0x7C, 0xFF, 0xFF, 0xF7, 0xEF, 0x80, 0xF0, 0xE1, 0xFF, 0x3E, 0x7F,
0xE7, 0xCF, 0xF8, 0x70, 0xF0, 0x3E, 0x1E, 0x00, 0x0F, 0xE3, 0xC0, 0x01, 0xDC, 0xF0, 0x00, 0x7B, 0xDE, 0x00, 0x0F,
0x7F, 0x80, 0x00, 0xEE, 0xF0, 0x00, 0x1F, 0xFC, 0x00, 0x01, 0xF7, 0xFE, 0x7E, 0x01, 0xFF, 0xCF, 0xE0, 0x3B, 0xFF,
0xFC, 0x0F, 0x73, 0xF3, 0x83, 0xCE, 0x7E, 0x70, 0x79, 0xFF, 0xFE, 0x1E, 0x3F, 0x9F, 0xC3, 0xC3, 0xF3, 0xF0, 0x00,
0x38, 0xF3, 0xE7, 0x9F, 0x1E, 0x3E, 0x3C, 0x38, 0x00, 0x00, 0xE3, 0xE3, 0xE3, 0xC7, 0xCF, 0x3E, 0xF8, 0xE0, 0x00,
0x01, 0xE0, 0x3C, 0x0F, 0x01, 0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xF0,
0x3C, 0x00, 0x07, 0xF0, 0xFF, 0x1F, 0xF1, 0xF6, 0x3C, 0x07, 0xFE, 0x7F, 0xC3, 0xC0, 0x7F, 0xC7, 0xFC, 0x3C, 0x03,
0xF7, 0x1F, 0xF0, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0x0F, 0xC0, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F,
0xC3, 0xFC, 0x1F, 0xC3, 0xF0, 0x3F, 0x03, 0xF0, 0x0F, 0x00, 0x3F, 0x87, 0xFC, 0x3F, 0xE3, 0xBE, 0xFF, 0xFF, 0xFF,
0x1F, 0x83, 0xF0, 0xFF, 0xFF, 0xFF, 0x78, 0x07, 0xDE, 0x7F, 0xE3, 0xFE, 0x1F, 0xE0, 0xFF, 0xFF, 0xFF, 0xC1, 0xF0,
0x07, 0xCF, 0xFF, 0x7F, 0xF8, 0x0F, 0x01, 0xF8, 0xFF, 0x87, 0xF8, 0x1F, 0x00, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x1F,
0x00, 0x3F, 0x83, 0xFC, 0x3F, 0xE3, 0xBF, 0x00, 0xF1, 0xFF, 0x3F, 0xF7, 0xFF, 0x7D, 0xFF, 0x0F, 0xF0, 0xFF, 0xBE,
0x7F, 0xE7, 0xFC, 0x3F, 0x80, 0x07, 0x80, 0x0F, 0x80, 0x3F, 0x00, 0x7F, 0x01, 0xFE, 0x03, 0xDC, 0x07, 0xBC, 0x1E,
0x78, 0x3C, 0x78, 0x78, 0xF1, 0xE1, 0xE3, 0xC1, 0xE7, 0xFF, 0xDF, 0xFF, 0xBF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F,
0xC1, 0xFE, 0x0F, 0xF0, 0x78, 0xFF, 0xFF, 0xFF, 0xFF, 0xBC, 0x07, 0xC0, 0x7C, 0x07, 0x80, 0xF8, 0x0F, 0x83, 0xE0,
0x78, 0x1E, 0x07, 0xC0, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xE0, 0x3C, 0x0F, 0x01,
0xE0, 0x78, 0x0F, 0x03, 0xC0, 0x78, 0x1E, 0x03, 0xC0, 0xF0, 0x1E, 0x07, 0x80, 0xF0, 0x3C, 0x00, 0x77, 0xFE, 0xE0,
0x00, 0x3C, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0F, 0x0F, 0x38, 0xFD, 0xE3, 0xF7, 0x87, 0xFC,
0x07, 0xF0, 0x0F, 0xC0, 0x3E, 0x00, 0x78, 0x00, 0x7D, 0xF3, 0xFF, 0xFF, 0xFF, 0xEE, 0x7F, 0xFF, 0xBF, 0xFD, 0xF7,
0xC0, 0x00, 0x07, 0xC7, 0xE7, 0xF3, 0xE1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78,
0x7C, 0x7E, 0x3F, 0x1F, 0x00, 0x00, 0x3C, 0x77, 0xFF, 0x7F, 0xEF, 0xFE, 0xE3, 0xC0, 0x00, 0x3C, 0x77, 0xFF, 0x7F,
0xEF, 0xFE, 0xE3, 0xC0, 0x03, 0x80, 0xE0, 0x7B, 0xFF, 0xFF, 0xFF, 0xF0, 0xE0, 0x70, 0xFF, 0xFF, 0xFF, 0xFD, 0xC0,
0x70, 0x1C, 0x00, 0x07, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x3F, 0xEF, 0xFD, 0xFF, 0x07, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF,
0xC0, 0xF8, 0x3F, 0xCF, 0xFD, 0xFF, 0x0F, 0xDF, 0xFF, 0xFF, 0xFE, 0xF8, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xC0,
};
static const EpdGlyph ubuntu_bold_10Glyphs[] = {
{0, 0, 0, 0, 0, 0, 0}, //
{0, 0, 0, 0, 0, 0, 0}, // 
{0, 0, 5, 0, 0, 0, 0}, //
{0, 0, 5, 0, 0, 0, 0}, //
{0, 0, 0, 0, 0, 0, 0}, // 
{0, 0, 5, 0, 0, 0, 0}, //
{4, 15, 6, 1, 15, 8, 0}, // !
{8, 6, 10, 1, 16, 6, 8}, // "
{13, 15, 15, 1, 15, 25, 14}, // #
{11, 19, 12, 0, 16, 27, 39}, // $
{19, 15, 19, 0, 15, 36, 66}, // %
{15, 15, 15, 0, 15, 29, 102}, // &
{4, 6, 5, 1, 16, 3, 131}, // '
{7, 20, 7, 1, 16, 18, 134}, // (
{6, 20, 7, 0, 16, 15, 152}, // )
{10, 8, 11, 0, 15, 10, 167}, // *
{10, 11, 12, 1, 12, 14, 177}, // +
{5, 6, 5, 0, 3, 4, 191}, // ,
{7, 3, 7, 0, 8, 3, 195}, // -
{5, 4, 5, 0, 4, 3, 198}, // .
{11, 20, 9, -1, 16, 28, 201}, // /
{12, 15, 12, 0, 15, 23, 229}, // 0
{8, 15, 12, 1, 15, 15, 252}, // 1
{11, 15, 12, 0, 15, 21, 267}, // 2
{11, 15, 12, 0, 15, 21, 288}, // 3
{12, 15, 12, 0, 15, 23, 309}, // 4
{11, 15, 12, 0, 15, 21, 332}, // 5
{12, 15, 12, 0, 15, 23, 353}, // 6
{12, 15, 12, 0, 15, 23, 376}, // 7
{12, 15, 12, 0, 15, 23, 399}, // 8
{12, 15, 12, 0, 15, 23, 422}, // 9
{5, 11, 5, 0, 11, 7, 445}, // :
{5, 14, 5, 0, 11, 9, 452}, // ;
{10, 11, 12, 1, 12, 14, 461}, // <
{10, 8, 12, 1, 10, 10, 475}, // =
{10, 11, 12, 1, 12, 14, 485}, // >
{10, 16, 10, 0, 16, 20, 499}, // ?
{19, 18, 20, 1, 15, 43, 519}, // @
{15, 15, 15, 0, 15, 29, 562}, // A
{13, 15, 14, 1, 15, 25, 591}, // B
{13, 15, 14, 1, 15, 25, 616}, // C
{14, 15, 15, 1, 15, 27, 641}, // D
{11, 15, 13, 1, 15, 21, 668}, // E
{11, 15, 12, 1, 15, 21, 689}, // F
{13, 15, 15, 1, 15, 25, 710}, // G
{13, 15, 15, 1, 15, 25, 735}, // H
{4, 15, 7, 1, 15, 8, 760}, // I
{10, 15, 11, 0, 15, 19, 768}, // J
{14, 15, 14, 1, 15, 27, 787}, // K
{11, 15, 12, 1, 15, 21, 814}, // L
{17, 15, 19, 1, 15, 32, 835}, // M
{14, 15, 16, 1, 15, 27, 867}, // N
{15, 15, 17, 1, 15, 29, 894}, // O
{12, 15, 14, 1, 15, 23, 923}, // P
{15, 18, 17, 1, 15, 34, 946}, // Q
{13, 15, 14, 1, 15, 25, 980}, // R
{12, 15, 12, 0, 15, 23, 1005}, // S
{13, 15, 13, 0, 15, 25, 1028}, // T
{13, 15, 15, 1, 15, 25, 1053}, // U
{15, 15, 15, 0, 15, 29, 1078}, // V
{20, 15, 20, 0, 15, 38, 1107}, // W
{14, 15, 14, 0, 15, 27, 1145}, // X
{14, 15, 14, 0, 15, 27, 1172}, // Y
{13, 15, 13, 0, 15, 25, 1199}, // Z
{7, 20, 8, 1, 16, 18, 1224}, // [
{11, 20, 9, -1, 16, 28, 1242}, // <backslash>
{6, 20, 8, 0, 16, 15, 1270}, // ]
{12, 9, 12, 0, 15, 14, 1285}, // ^
{11, 3, 11, 0, -1, 5, 1299}, // _
{6, 5, 6, 0, 17, 4, 1304}, // `
{11, 11, 12, 0, 11, 16, 1308}, // a
{11, 16, 13, 1, 16, 22, 1324}, // b
{10, 11, 11, 0, 11, 14, 1346}, // c
{12, 16, 13, 0, 16, 24, 1360}, // d
{12, 11, 12, 0, 11, 17, 1384}, // e
{9, 16, 9, 1, 16, 18, 1401}, // f
{11, 15, 12, 0, 11, 21, 1419}, // g
{10, 16, 12, 1, 16, 20, 1440}, // h
{4, 16, 6, 1, 16, 8, 1460}, // i
{7, 20, 6, -2, 16, 18, 1468}, // j
{11, 16, 12, 1, 16, 22, 1486}, // k
{6, 16, 7, 1, 16, 12, 1508}, // l
{16, 11, 18, 1, 11, 22, 1520}, // m
{10, 11, 12, 1, 11, 14, 1542}, // n
{12, 11, 13, 0, 11, 17, 1556}, // o
{11, 15, 13, 1, 11, 21, 1573}, // p
{12, 15, 13, 0, 11, 23, 1594}, // q
{8, 11, 9, 1, 11, 11, 1617}, // r
{10, 11, 10, 0, 11, 14, 1628}, // s
{8, 14, 9, 1, 14, 14, 1642}, // t
{10, 11, 12, 1, 11, 14, 1656}, // u
{12, 11, 12, 0, 11, 17, 1670}, // v
{17, 11, 16, 0, 11, 24, 1687}, // w
{12, 11, 12, 0, 11, 17, 1711}, // x
{12, 15, 11, 0, 11, 23, 1728}, // y
{10, 11, 11, 0, 11, 14, 1751}, // z
{8, 20, 8, 0, 16, 20, 1765}, // {
{4, 20, 7, 1, 16, 10, 1785}, // |
{8, 20, 8, 0, 16, 20, 1795}, // }
{12, 5, 12, 0, 9, 8, 1815}, // ~
{0, 0, 5, 0, 0, 0, 1823}, //  
{4, 15, 6, 1, 11, 8, 1823}, // ¡
{10, 16, 12, 0, 14, 20, 1831}, // ¢
{11, 15, 12, 0, 15, 21, 1851}, // £
{12, 11, 12, 0, 13, 17, 1872}, // ¤
{14, 15, 12, -1, 15, 27, 1889}, // ¥
{4, 20, 6, 1, 16, 10, 1916}, // ¦
{11, 19, 11, 0, 15, 27, 1926}, // §
{9, 3, 11, 1, 16, 4, 1953}, // ¨
{15, 15, 17, 1, 15, 29, 1957}, // ©
{8, 8, 8, 0, 15, 8, 1986}, // ª
{12, 11, 13, 0, 11, 17, 1994}, // «
{10, 8, 12, 1, 9, 10, 2011}, // ¬
{7, 3, 7, 0, 8, 3, 2021}, // ­
{15, 15, 17, 1, 15, 29, 2024}, // ®
{8, 2, 8, 0, 15, 2, 2053}, // ¯
{8, 6, 8, 0, 16, 6, 2055}, // °
{10, 15, 12, 1, 15, 19, 2061}, // ±
{7, 9, 8, 0, 15, 8, 2080}, // ²
{7, 9, 8, 0, 15, 8, 2088}, // ³
{6, 5, 6, 0, 17, 4, 2096}, // ´
{10, 15, 12, 1, 11, 19, 2100}, // µ
{14, 19, 15, 0, 15, 34, 2119}, // ¶
{5, 4, 5, 0, 9, 3, 2153}, // ·
{6, 6, 7, 0, 1, 5, 2156}, // ¸
{6, 9, 8, 0, 15, 7, 2161}, // ¹
{9, 8, 10, 0, 15, 9, 2168}, // º
{13, 11, 13, 0, 11, 18, 2177}, // »
{19, 15, 18, 0, 15, 36, 2195}, // ¼
{18, 15, 18, 0, 15, 34, 2231}, // ½
{19, 15, 18, 0, 15, 36, 2265}, // ¾
{10, 15, 10, 0, 11, 19, 2301}, // ¿
{15, 21, 15, 0, 21, 40, 2320}, // À
{15, 21, 15, 0, 21, 40, 2360}, // Á
{15, 21, 15, 0, 21, 40, 2400}, // Â
{15, 19, 15, 0, 19, 36, 2440}, // Ã
{15, 19, 15, 0, 19, 36, 2476}, // Ä
{15, 19, 15, 0, 19, 36, 2512}, // Å
{21, 15, 21, 0, 15, 40, 2548}, // Æ
{13, 20, 14, 1, 15, 33, 2588}, // Ç
{11, 21, 13, 1, 21, 29, 2621}, // È
{11, 21, 13, 1, 21, 29, 2650}, // É
{11, 21, 13, 1, 21, 29, 2679}, // Ê
{11, 19, 13, 1, 19, 27, 2708}, // Ë
{6, 21, 7, 0, 21, 16, 2735}, // Ì
{5, 21, 7, 1, 21, 14, 2751}, // Í
{7, 21, 7, 0, 21, 19, 2765}, // Î
{9, 19, 7, -1, 19, 22, 2784}, // Ï
{15, 15, 16, 0, 15, 29, 2806}, // Ð
{14, 19, 16, 1, 19, 34, 2835}, // Ñ
{15, 21, 17, 1, 21, 40, 2869}, // Ò
{15, 21, 17, 1, 21, 40, 2909}, // Ó
{15, 21, 17, 1, 21, 40, 2949}, // Ô
{15, 19, 17, 1, 19, 36, 2989}, // Õ
{15, 19, 17, 1, 19, 36, 3025}, // Ö
{10, 9, 12, 1, 11, 12, 3061}, // ×
{15, 17, 17, 1, 16, 32, 3073}, // Ø
{13, 21, 15, 1, 21, 35, 3105}, // Ù
{13, 21, 15, 1, 21, 35, 3140}, // Ú
{13, 21, 15, 1, 21, 35, 3175}, // Û
{13, 19, 15, 1, 19, 31, 3210}, // Ü
{14, 21, 14, 0, 21, 37, 3241}, // Ý
{12, 15, 14, 1, 15, 23, 3278}, // Þ
{13, 16, 14, 1, 16, 26, 3301}, // ß
{11, 17, 12, 0, 17, 24, 3327}, // à
{11, 17, 12, 0, 17, 24, 3351}, // á
{11, 17, 12, 0, 17, 24, 3375}, // â
{11, 16, 12, 0, 16, 22, 3399}, // ã
{11, 16, 12, 0, 16, 22, 3421}, // ä
{11, 18, 12, 0, 18, 25, 3443}, // å
{18, 11, 18, 0, 11, 25, 3468}, // æ
{10, 15, 11, 0, 11, 19, 3493}, // ç
{12, 17, 12, 0, 17, 26, 3512}, // è
{12, 17, 12, 0, 17, 26, 3538}, // é
{12, 17, 12, 0, 17, 26, 3564}, // ê
{12, 16, 12, 0, 16, 24, 3590}, // ë
{6, 17, 6, 0, 17, 13, 3614}, // ì
{6, 17, 6, 0, 17, 13, 3627}, // í
{8, 17, 6, -1, 17, 17, 3640}, // î
{9, 16, 6, -2, 16, 18, 3657}, // ï
{13, 16, 13, 0, 16, 26, 3675}, // ð
{10, 16, 12, 1, 16, 20, 3701}, // ñ
{12, 17, 13, 0, 17, 26, 3721}, // ò
{12, 17, 13, 0, 17, 26, 3747}, // ó
{12, 17, 13, 0, 17, 26, 3773}, // ô
{12, 16, 13, 0, 16, 24, 3799}, // õ
{12, 16, 13, 0, 16, 24, 3823}, // ö
{10, 13, 12, 1, 13, 17, 3847}, // ÷
{12, 13, 13, 0, 12, 20, 3864}, // ø
{10, 17, 12, 1, 17, 22, 3884}, // ù
{10, 17, 12, 1, 17, 22, 3906}, // ú
{10, 17, 12, 1, 17, 22, 3928}, // û
{10, 16, 12, 1, 16, 20, 3950}, // ü
{12, 21, 11, 0, 17, 32, 3970}, // ý
{11, 20, 13, 1, 16, 28, 4002}, // þ
{12, 20, 11, 0, 16, 30, 4030}, // ÿ
{15, 18, 15, 0, 18, 34, 4060}, // Ā
{11, 15, 12, 0, 15, 21, 4094}, // ā
{15, 19, 15, 0, 19, 36, 4115}, // Ă
{11, 16, 12, 0, 16, 22, 4151}, // ă
{16, 20, 15, 0, 15, 40, 4173}, // Ą
{11, 15, 12, 0, 11, 21, 4213}, // ą
{13, 21, 14, 1, 21, 35, 4234}, // Ć
{10, 17, 11, 0, 17, 22, 4269}, // ć
{13, 21, 14, 1, 21, 35, 4291}, // Ĉ
{10, 17, 11, 0, 17, 22, 4326}, // ĉ
{13, 19, 14, 1, 19, 31, 4348}, // Ċ
{10, 16, 11, 0, 16, 20, 4379}, // ċ
{13, 21, 14, 1, 21, 35, 4399}, // Č
{10, 17, 11, 0, 17, 22, 4434}, // č
{14, 21, 15, 1, 21, 37, 4456}, // Ď
{16, 16, 14, 0, 16, 32, 4493}, // ď
{15, 15, 16, 0, 15, 29, 4525}, // Đ
{13, 16, 13, 0, 16, 26, 4554}, // đ
{11, 18, 13, 1, 18, 25, 4580}, // Ē
{12, 15, 12, 0, 15, 23, 4605}, // ē
{11, 19, 13, 1, 19, 27, 4628}, // Ĕ
{12, 16, 12, 0, 16, 24, 4655}, // ĕ
{11, 19, 13, 1, 19, 27, 4679}, // Ė
{12, 16, 12, 0, 16, 24, 4706}, // ė
{12, 20, 13, 1, 15, 30, 4730}, // Ę
{12, 15, 12, 0, 11, 23, 4760}, // ę
{11, 21, 13, 1, 21, 29, 4783}, // Ě
{12, 17, 12, 0, 17, 26, 4812}, // ě
{13, 21, 15, 1, 21, 35, 4838}, // Ĝ
{11, 21, 12, 0, 17, 29, 4873}, // ĝ
{13, 19, 15, 1, 19, 31, 4902}, // Ğ
{11, 20, 12, 0, 16, 28, 4933}, // ğ
{13, 19, 15, 1, 19, 31, 4961}, // Ġ
{11, 20, 12, 0, 16, 28, 4992}, // ġ
{13, 20, 15, 1, 15, 33, 5020}, // Ģ
{11, 21, 12, 0, 17, 29, 5053}, // ģ
{13, 21, 15, 1, 21, 35, 5082}, // Ĥ
{10, 20, 12, 1, 20, 25, 5117}, // ĥ
{16, 15, 16, 0, 15, 30, 5142}, // Ħ
{12, 16, 13, 0, 16, 24, 5172}, // ħ
{9, 19, 7, -1, 19, 22, 5196}, // Ĩ
{8, 16, 6, -1, 16, 16, 5218}, // ĩ
{8, 18, 7, -1, 18, 18, 5234}, // Ī
{8, 15, 6, -1, 15, 15, 5252}, // ī
{7, 19, 7, 0, 19, 17, 5267}, // Ĭ
{8, 16, 6, -1, 16, 16, 5284}, // ĭ
{6, 20, 7, 0, 15, 15, 5300}, // Į
{6, 21, 6, 0, 16, 16, 5315}, // į
{4, 19, 7, 1, 19, 10, 5331}, // İ
{4, 11, 6, 1, 11, 6, 5341}, // ı
{16, 15, 18, 1, 15, 30, 5347}, // IJ
{10, 20, 12, 1, 16, 25, 5377}, // ij
{12, 21, 11, 0, 21, 32, 5402}, // Ĵ
{9, 21, 6, -2, 17, 24, 5434}, // ĵ
{14, 20, 14, 1, 15, 35, 5458}, // Ķ
{11, 21, 12, 1, 16, 29, 5493}, // ķ
{12, 11, 12, 1, 11, 17, 5522}, // ĸ
{11, 21, 12, 1, 21, 29, 5539}, // Ĺ
{6, 21, 7, 1, 21, 16, 5568}, // ĺ
{11, 20, 12, 1, 15, 28, 5584}, // Ļ
{6, 21, 7, 1, 16, 16, 5612}, // ļ
{11, 15, 12, 1, 15, 21, 5628}, // Ľ
{8, 16, 8, 1, 16, 16, 5649}, // ľ
{11, 15, 12, 1, 15, 21, 5665}, // Ŀ
{10, 16, 11, 1, 16, 20, 5686}, // ŀ
{13, 15, 12, 0, 15, 25, 5706}, // Ł
{9, 16, 8, -1, 16, 18, 5731}, // ł
{14, 21, 16, 1, 21, 37, 5749}, // Ń
{10, 17, 12, 1, 17, 22, 5786}, // ń
{14, 20, 16, 1, 15, 35, 5808}, // Ņ
{10, 16, 12, 1, 11, 20, 5843}, // ņ
{14, 21, 16, 1, 21, 37, 5863}, // Ň
{10, 17, 12, 1, 17, 22, 5900}, // ň
{13, 16, 13, -1, 16, 26, 5922}, // ʼn
{14, 19, 16, 1, 15, 34, 5948}, // Ŋ
{10, 15, 12, 1, 11, 19, 5982}, // ŋ
{15, 18, 17, 1, 18, 34, 6001}, // Ō
{12, 15, 13, 0, 15, 23, 6035}, // ō
{15, 19, 17, 1, 19, 36, 6058}, // Ŏ
{12, 16, 13, 0, 16, 24, 6094}, // ŏ
{15, 21, 17, 1, 21, 40, 6118}, // Ő
{12, 17, 13, 0, 17, 26, 6158}, // ő
{20, 15, 21, 1, 15, 38, 6184}, // Œ
{19, 11, 20, 0, 11, 27, 6222}, // œ
{13, 21, 14, 1, 21, 35, 6249}, // Ŕ
{8, 17, 9, 1, 17, 17, 6284}, // ŕ
{13, 20, 14, 1, 15, 33, 6301}, // Ŗ
{9, 16, 9, 0, 11, 18, 6334}, // ŗ
{13, 20, 14, 1, 20, 33, 6352}, // Ř
{8, 17, 9, 1, 17, 17, 6385}, // ř
{12, 21, 12, 0, 21, 32, 6402}, // Ś
{10, 17, 10, 0, 17, 22, 6434}, // ś
{12, 21, 12, 0, 21, 32, 6456}, // Ŝ
{10, 17, 10, 0, 17, 22, 6488}, // ŝ
{12, 20, 12, 0, 15, 30, 6510}, // Ş
{10, 16, 10, 0, 11, 20, 6540}, // ş
{12, 21, 12, 0, 21, 32, 6560}, // Š
{10, 17, 10, 0, 17, 22, 6592}, // š
{13, 19, 13, 0, 15, 31, 6614}, // Ţ
{8, 18, 9, 1, 14, 18, 6645}, // ţ
{13, 21, 13, 0, 21, 35, 6663}, // Ť
{8, 16, 9, 1, 16, 16, 6698}, // ť
{13, 15, 13, 0, 15, 25, 6714}, // Ŧ
{8, 14, 9, 1, 14, 14, 6739}, // ŧ
{13, 19, 15, 1, 19, 31, 6753}, // Ũ
{10, 16, 12, 1, 16, 20, 6784}, // ũ
{13, 18, 15, 1, 18, 30, 6804}, // Ū
{10, 15, 12, 1, 15, 19, 6834}, // ū
{13, 19, 15, 1, 19, 31, 6853}, // Ŭ
{10, 16, 12, 1, 16, 20, 6884}, // ŭ
{13, 21, 15, 1, 21, 35, 6904}, // Ů
{10, 18, 12, 1, 18, 23, 6939}, // ů
{13, 21, 15, 1, 21, 35, 6962}, // Ű
{11, 17, 12, 1, 17, 24, 6997}, // ű
{13, 19, 15, 1, 15, 31, 7021}, // Ų
{10, 15, 12, 1, 11, 19, 7052}, // ų
{20, 21, 20, 0, 21, 53, 7071}, // Ŵ
{17, 17, 16, 0, 17, 37, 7124}, // ŵ
{14, 21, 14, 0, 21, 37, 7161}, // Ŷ
{12, 21, 11, 0, 17, 32, 7198}, // ŷ
{14, 19, 14, 0, 19, 34, 7230}, // Ÿ
{13, 21, 13, 0, 21, 35, 7264}, // Ź
{10, 17, 11, 0, 17, 22, 7299}, // ź
{13, 19, 13, 0, 19, 31, 7321}, // Ż
{10, 16, 11, 0, 16, 20, 7352}, // ż
{13, 21, 13, 0, 21, 35, 7372}, // Ž
{10, 17, 11, 0, 17, 22, 7407}, // ž
{9, 16, 7, 1, 16, 18, 7429}, // ſ
{8, 3, 8, 0, 15, 3, 7447}, // ̑
{11, 3, 11, 0, 8, 5, 7450}, //
{21, 3, 21, 0, 8, 8, 7455}, // —
{21, 3, 21, 0, 8, 8, 7463}, // ―
{5, 6, 5, 0, 16, 4, 7471}, //
{5, 6, 5, 0, 16, 4, 7475}, //
{5, 6, 5, 0, 3, 4, 7479}, //
{10, 6, 10, 0, 16, 8, 7483}, // “
{9, 6, 10, 0, 16, 7, 7491}, // ”
{9, 6, 10, 0, 3, 7, 7498}, // „
{10, 18, 10, 0, 15, 23, 7505}, // †
{10, 18, 10, 0, 15, 23, 7528}, // ‡
{7, 6, 8, 0, 10, 6, 7551}, // •
{19, 4, 21, 1, 4, 10, 7557}, // …
{27, 15, 28, 0, 15, 51, 7567}, // ‰
{7, 11, 7, 0, 11, 10, 7618}, //
{7, 11, 7, 0, 11, 10, 7628}, //
{11, 15, 3, -4, 15, 21, 7638}, //
{12, 15, 12, 0, 15, 23, 7659}, // €
{12, 15, 12, 0, 15, 23, 7682}, // ₮
{12, 15, 12, 0, 15, 23, 7705}, // ₴
{13, 15, 12, 0, 15, 25, 7728}, // ₹
{12, 15, 13, 0, 15, 23, 7753}, // ∂
{15, 15, 15, 0, 15, 29, 7776}, // ∆
{13, 17, 15, 1, 15, 28, 7805}, // ∏
{11, 17, 11, 0, 15, 24, 7833}, // ∑
{10, 3, 12, 1, 8, 4, 7857}, //
{11, 15, 3, -4, 15, 21, 7861}, //
{5, 4, 5, 0, 9, 3, 7882}, // ∙
{14, 15, 13, 0, 15, 27, 7885}, // √
{13, 7, 13, 0, 10, 12, 7912}, // ∞
{9, 21, 8, 0, 16, 24, 7924}, // ∫
{12, 11, 12, 0, 12, 17, 7948}, // ≈
{10, 14, 12, 1, 14, 18, 7965}, // ≠
{10, 13, 12, 1, 13, 17, 7983}, // ≤
{10, 13, 12, 1, 13, 17, 8000}, // ≥
};
static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
{0x0, 0x0, 0x0}, {0x8, 0x9, 0x1}, {0xD, 0xD, 0x3}, {0x1D, 0x1D, 0x4},
{0x20, 0x7E, 0x5}, {0xA0, 0xFF, 0x64}, {0x100, 0x17F, 0xC4}, {0x311, 0x311, 0x144},
{0x2013, 0x2015, 0x145}, {0x2018, 0x201A, 0x148}, {0x201C, 0x201E, 0x14B}, {0x2020, 0x2022, 0x14E},
{0x2026, 0x2026, 0x151}, {0x2030, 0x2030, 0x152}, {0x2039, 0x203A, 0x153}, {0x2044, 0x2044, 0x155},
{0x20AC, 0x20AC, 0x156}, {0x20AE, 0x20AE, 0x157}, {0x20B4, 0x20B4, 0x158}, {0x20B9, 0x20B9, 0x159},
{0x2202, 0x2202, 0x15A}, {0x2206, 0x2206, 0x15B}, {0x220F, 0x220F, 0x15C}, {0x2211, 0x2212, 0x15D},
{0x2215, 0x2215, 0x15F}, {0x2219, 0x221A, 0x160}, {0x221E, 0x221E, 0x162}, {0x222B, 0x222B, 0x163},
{0x2248, 0x2248, 0x164}, {0x2260, 0x2260, 0x165}, {0x2264, 0x2265, 0x166},
};
static const EpdFontData ubuntu_bold_10 = {
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4,
};

View File

@@ -1,247 +0,0 @@
#!python3
import freetype
import zlib
import sys
import re
import math
import argparse
from collections import namedtuple
# Originally from https://github.com/vroland/epdiy
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
parser.add_argument("name", action="store", help="name of the font.")
parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
size = args.size
font_name = args.name
# inclusive unicode code point intervals
# must not overlap and be in ascending order
intervals = [
### Basic Latin ###
# ASCII letters, digits, punctuation, control characters
(0x0000, 0x007F),
### Latin-1 Supplement ###
# Accented characters for Western European languages
(0x0080, 0x00FF),
### Latin Extended-A ###
# Eastern European and Baltic languages
(0x0100, 0x017F),
### General Punctuation (core subset) ###
# Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
(0x2000, 0x206F),
### Basic Symbols From "Latin-1 + Misc" ###
# dashes, quotes, prime marks
(0x2010, 0x203A),
# misc punctuation
(0x2040, 0x205F),
# common currency symbols
(0x20A0, 0x20CF),
### Combining Diacritical Marks (minimal subset) ###
# Needed for proper rendering of many extended Latin languages
(0x0300, 0x036F),
### Greek & Coptic ###
# Used in science, maths, philosophy, some academic texts
# (0x0370, 0x03FF),
### Cyrillic ###
# Russian, Ukrainian, Bulgarian, etc.
# (0x0400, 0x04FF),
### Math Symbols (common subset) ###
# General math operators
(0x2200, 0x22FF),
# Arrows
(0x2190, 0x21FF),
### CJK ###
# Core Unified Ideographs
# (0x4E00, 0x9FFF),
# # Extension A
# (0x3400, 0x4DBF),
# # Extension B
# (0x20000, 0x2A6DF),
# # Extension CF
# (0x2A700, 0x2EBEF),
# # Extension G
# (0x30000, 0x3134F),
# # Hiragana
# (0x3040, 0x309F),
# # Katakana
# (0x30A0, 0x30FF),
# # Katakana Phonetic Extensions
# (0x31F0, 0x31FF),
# # Halfwidth Katakana
# (0xFF60, 0xFF9F),
# # Hangul Syllables
# (0xAC00, 0xD7AF),
# # Hangul Jamo
# (0x1100, 0x11FF),
# # Hangul Compatibility Jamo
# (0x3130, 0x318F),
# # Hangul Jamo Extended-A
# (0xA960, 0xA97F),
# # Hangul Jamo Extended-B
# (0xD7B0, 0xD7FF),
# # CJK Radicals Supplement
# (0x2E80, 0x2EFF),
# # Kangxi Radicals
# (0x2F00, 0x2FDF),
# # CJK Symbols and Punctuation
# (0x3000, 0x303F),
# # CJK Compatibility Forms
# (0xFE30, 0xFE4F),
# # CJK Compatibility Ideographs
# (0xF900, 0xFAFF),
]
add_ints = []
if args.additional_intervals:
add_ints = [tuple([int(n, base=0) for n in i.split(",")]) for i in args.additional_intervals]
def norm_floor(val):
return int(math.floor(val / (1 << 6)))
def norm_ceil(val):
return int(math.ceil(val / (1 << 6)))
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i + n]
def load_glyph(code_point):
face_index = 0
while face_index < len(font_stack):
face = font_stack[face_index]
glyph_index = face.get_char_index(code_point)
if glyph_index > 0:
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
return face
face_index += 1
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
return None
unmerged_intervals = sorted(intervals + add_ints)
intervals = []
unvalidated_intervals = []
for i_start, i_end in unmerged_intervals:
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
continue
unvalidated_intervals.append((i_start, i_end))
for i_start, i_end in unvalidated_intervals:
start = i_start
for code_point in range(i_start, i_end + 1):
face = load_glyph(code_point)
if face is None:
if start < code_point:
intervals.append((start, code_point - 1))
start = code_point + 1
if start != i_end + 1:
intervals.append((start, i_end))
for face in font_stack:
face.set_char_size(size << 6, size << 6, 150, 150)
total_size = 0
all_glyphs = []
for i_start, i_end in intervals:
for code_point in range(i_start, i_end + 1):
face = load_glyph(code_point)
bitmap = face.glyph.bitmap
# Build out 4-bit greyscale bitmap
pixels4g = []
px = 0
for i, v in enumerate(bitmap.buffer):
y = i / bitmap.width
x = i % bitmap.width
if x % 2 == 0:
px = (v >> 4)
else:
px = px | (v & 0xF0)
pixels4g.append(px);
px = 0
# eol
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
pixels4g.append(px)
px = 0
# Downsample to 1-bit bitmap - treat any non-zero as black
pixelsbw = []
px = 0
pitch = (bitmap.width // 2) + (bitmap.width % 2)
for y in range(bitmap.rows):
for x in range(bitmap.width):
px = px << 1
bm = pixels4g[y * pitch + (x // 2)]
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
if (y * bitmap.width + x) % 8 == 7:
pixelsbw.append(px)
px = 0
if (bitmap.width * bitmap.rows) % 8 != 0:
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px)
# Build output data
packed = bytes(pixelsbw)
glyph = GlyphProps(
width = bitmap.width,
height = bitmap.rows,
advance_x = norm_floor(face.glyph.advance.x),
left = face.glyph.bitmap_left,
top = face.glyph.bitmap_top,
data_length = len(packed),
data_offset = total_size,
code_point = code_point,
)
total_size += len(packed)
all_glyphs.append((glyph, packed))
# pipe seems to be a good heuristic for the "real" descender
face = load_glyph(ord('|'))
glyph_data = []
glyph_props = []
for index, glyph in enumerate(all_glyphs):
props, packed = glyph
glyph_data.extend([b for b in packed])
glyph_props.append(props)
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n */")
print("#pragma once")
print("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
for c in chunks(glyph_data, 16):
print (" " + " ".join(f"0x{b:02X}," for b in c))
print ("};\n");
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
for i, g in enumerate(glyph_props):
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
print ("};\n");
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
offset = 0
for i_start, i_end in intervals:
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
offset += i_end - i_start + 1
print ("};\n");
print(f"static const EpdFontData {font_name} = {{")
print(f" {font_name}Bitmaps,")
print(f" {font_name}Glyphs,")
print(f" {font_name}Intervals,")
print(f" {len(intervals)},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print("};")

View File

@@ -1 +0,0 @@
freetype-py==2.5.1

View File

@@ -1,84 +0,0 @@
#pragma once
#include <EpdFontFamily.h>
#include <HardwareSerial.h>
#include <Utf8.h>
inline int min(const int a, const int b) { return a < b ? a : b; }
inline int max(const int a, const int b) { return a > b ? a : b; }
template <typename Renderable>
class EpdFontRenderer {
Renderable& renderer;
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
public:
const EpdFontFamily* fontFamily;
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
: fontFamily(fontFamily), renderer(renderer) {}
~EpdFontRenderer() = default;
void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR);
};
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
const EpdFontStyle style) {
// cannot draw a NULL / empty string
if (string == nullptr || *string == '\0') {
return;
}
// no printable characters
if (!fontFamily->hasPrintableChars(string, style)) {
return;
}
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
renderChar(cp, x, y, color, style);
}
*y += fontFamily->getData(style)->advanceY;
}
template <typename Renderable>
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
const EpdFontStyle style) {
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
if (!glyph) {
// TODO: Replace with fallback glyph property?
glyph = fontFamily->getGlyph('?', style);
}
// no glyph?
if (!glyph) {
Serial.printf("No glyph for codepoint %d\n", cp);
return;
}
const uint32_t offset = glyph->dataOffset;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
const int left = glyph->left;
const uint8_t* bitmap = nullptr;
bitmap = &fontFamily->getData(style)->bitmap[offset];
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {
int screenY = *y - glyph->top + glyphY;
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
int screenX = *x + left + glyphX;
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
renderer.drawPixel(screenX, screenY, color);
}
}
}
}
*x += glyph->advanceX;
}

View File

@@ -1,168 +0,0 @@
#include "EpdRenderer.h"
#include "builtinFonts/babyblue.h"
#include "builtinFonts/bookerly.h"
#include "builtinFonts/bookerly_bold.h"
#include "builtinFonts/bookerly_bold_italic.h"
#include "builtinFonts/bookerly_italic.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
EpdFont bookerlyFont(&bookerly);
EpdFont bookerlyBoldFont(&bookerly_bold);
EpdFont bookerlyItalicFont(&bookerly_italic);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue);
EpdFontFamily smallFontFamily(&smallFont);
EpdFont ubuntu10Font(&ubuntu_10);
EpdFont ununtuBold10Font(&ubuntu_bold_10);
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
EpdRenderer::EpdRenderer(XteinkDisplay& display)
: display(display), marginTop(11), marginBottom(30), marginLeft(10), marginRight(10), lineCompression(0.95f) {
this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(&bookerlyFontFamily, display);
this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(&smallFontFamily, display);
this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(&ubuntuFontFamily, display);
}
EpdRenderer::~EpdRenderer() {
delete regularFontRenderer;
delete smallFontRenderer;
delete uiFontRenderer;
}
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
int w = 0, h = 0;
regularFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w;
}
int EpdRenderer::getUiTextWidth(const char* text, const EpdFontStyle style) const {
int w = 0, h = 0;
uiFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w;
}
int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) const {
int w = 0, h = 0;
smallFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
return w;
}
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
const EpdFontStyle style) const {
int ypos = y + getLineHeight() + marginTop;
int xpos = x + marginLeft;
regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
}
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
const EpdFontStyle style) const {
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
}
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
const EpdFontStyle style) const {
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
int xpos = x + marginLeft;
smallFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
}
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
const EpdFontStyle style) const {
const size_t length = text.length();
// fit the text into the box
int start = 0;
int end = 1;
int ypos = 0;
while (true) {
if (end >= length) {
drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
break;
}
if (ypos + getLineHeight() >= height) {
break;
}
if (text[end - 1] == '\n') {
drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
ypos += getLineHeight();
start = end;
end = start + 1;
continue;
}
if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), 1, style);
ypos += getLineHeight();
start = end - 1;
continue;
}
end++;
}
}
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
display.drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
display.drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
display.fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::drawCircle(const int x, const int y, const int radius, const uint16_t color) const {
display.drawCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::fillCircle(const int x, const int y, const int radius, const uint16_t color) const {
display.fillCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
const bool invert, const bool mirrorY) const {
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height, invert, mirrorY);
}
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
const bool invert, const bool mirrorY) const {
display.drawImage(bitmap, x, y, width, height, invert, mirrorY);
}
void EpdRenderer::clearScreen(const bool black) const {
Serial.println("Clearing screen");
display.fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
}
void EpdRenderer::flushDisplay(const bool partialUpdate) const { display.display(partialUpdate); }
void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
display.displayWindow(x + marginLeft, y + marginTop, width, height);
}
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
int EpdRenderer::getLineHeight() const {
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include <GxEPD2_BW.h>
#include <EpdFontRenderer.hpp>
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
class EpdRenderer {
XteinkDisplay& display;
EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
int marginTop;
int marginBottom;
int marginLeft;
int marginRight;
float lineCompression;
public:
explicit EpdRenderer(XteinkDisplay& display);
~EpdRenderer();
int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawSmallText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
void drawLine(int x1, int y1, int x2, int y2, uint16_t color = 1) const;
void drawRect(int x, int y, int width, int height, uint16_t color = 1) const;
void fillRect(int x, int y, int width, int height, uint16_t color = 1) const;
void drawCircle(int x, int y, int radius, uint16_t color = 1) const;
void fillCircle(int x, int y, int radius, uint16_t color = 1) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
bool mirrorY = false) const;
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
bool mirrorY = false) const;
void clearScreen(bool black = false) const;
void flushDisplay(bool partialUpdate = true) const;
void flushArea(int x, int y, int width, int height) const;
int getPageWidth() const;
int getPageHeight() const;
int getSpaceWidth() const;
int getLineHeight() const;
// set margins
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
};

View File

@@ -1,320 +1,508 @@
#include "Epub.h"
#include <FsHelpers.h>
#include <HardwareSerial.h>
#include <SD.h>
#include <JpegToBmpConverter.h>
#include <SDCardManager.h>
#include <ZipFile.h>
#include <map>
#include "Epub/parsers/ContainerParser.h"
#include "Epub/parsers/ContentOpfParser.h"
#include "Epub/parsers/TocNavParser.h"
#include "Epub/parsers/TocNcxParser.h"
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
// open up the meta data to find where the content.opf file lives
size_t s;
const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true));
if (!metaInfo) {
Serial.println("Could not find META-INF/container.xml");
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
const auto containerPath = "META-INF/container.xml";
size_t containerSize;
// Get file size without loading it all into heap
if (!getItemSize(containerPath, &containerSize)) {
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
return false;
}
// parse the meta data
tinyxml2::XMLDocument metaDataDoc;
const auto result = metaDataDoc.Parse(metaInfo);
free(metaInfo);
ContainerParser containerParser(containerSize);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("Could not parse META-INF/container.xml. Error: %d\n", result);
if (!containerParser.setup()) {
return false;
}
const auto container = metaDataDoc.FirstChildElement("container");
if (!container) {
Serial.println("Could not find container element in META-INF/container.xml");
// Stream read (reusing your existing stream logic)
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
return false;
}
const auto rootfiles = container->FirstChildElement("rootfiles");
if (!rootfiles) {
Serial.println("Could not find rootfiles element in META-INF/container.xml");
// Extract the result
if (containerParser.fullPath.empty()) {
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
return false;
}
// find the root file that has the media-type="application/oebps-package+xml"
auto rootfile = rootfiles->FirstChildElement("rootfile");
while (rootfile) {
const char* mediaType = rootfile->Attribute("media-type");
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
const char* full_path = rootfile->Attribute("full-path");
if (full_path) {
contentOpfFile = full_path;
return true;
}
}
rootfile = rootfile->NextSiblingElement("rootfile");
}
Serial.println("Could not get path to content.opf file");
return false;
}
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
// read in the content.opf file and parse it
auto contents = reinterpret_cast<char*>(zip.readFileToMemory(content_opf_file.c_str(), nullptr, true));
// parse the contents
tinyxml2::XMLDocument doc;
auto result = doc.Parse(contents);
free(contents);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("Error parsing content.opf - %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
return false;
}
auto package = doc.FirstChildElement("package");
if (!package) package = doc.FirstChildElement("opf:package");
if (!package) {
Serial.println("Could not find package element in content.opf");
return false;
}
// get the metadata - title and cover image
auto metadata = package->FirstChildElement("metadata");
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
if (!metadata) {
Serial.println("Missing metadata");
return false;
}
auto titleEl = metadata->FirstChildElement("dc:title");
if (!titleEl) {
Serial.println("Missing title");
return false;
}
this->title = titleEl->GetText();
auto cover = metadata->FirstChildElement("meta");
if (!cover) cover = metadata->FirstChildElement("opf:meta");
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
cover = cover->NextSiblingElement("meta");
}
if (!cover) {
Serial.println("Missing cover");
}
auto coverItem = cover ? cover->Attribute("content") : nullptr;
// read the manifest and spine
// the manifest gives us the names of the files
// the spine gives us the order of the files
// we can then read the files in the order they are in the spine
auto manifest = package->FirstChildElement("manifest");
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
if (!manifest) {
Serial.println("Missing manifest");
return false;
}
// create a mapping from id to file name
auto item = manifest->FirstChildElement("item");
if (!item) item = manifest->FirstChildElement("opf:item");
std::map<std::string, std::string> items;
while (item) {
std::string itemId = item->Attribute("id");
std::string href = contentBasePath + item->Attribute("href");
// grab the cover image
if (coverItem && itemId == coverItem) {
coverImageItem = href;
}
// grab the ncx file
if (itemId == "ncx" || itemId == "ncxtoc") {
tocNcxItem = href;
}
items[itemId] = href;
auto nextItem = item->NextSiblingElement("item");
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
item = nextItem;
}
// find the spine
auto spineEl = package->FirstChildElement("spine");
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
if (!spineEl) {
Serial.println("Missing spine");
return false;
}
// read the spine
auto itemref = spineEl->FirstChildElement("itemref");
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
while (itemref) {
auto id = itemref->Attribute("idref");
if (items.find(id) != items.end()) {
spine.emplace_back(id, items[id]);
}
auto nextItemRef = itemref->NextSiblingElement("itemref");
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
itemref = nextItemRef;
}
*contentOpfFile = std::move(containerParser.fullPath);
return true;
}
bool Epub::parseTocNcxFile(const ZipFile& zip) {
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
std::string contentOpfFilePath;
if (!findContentOpfFile(&contentOpfFilePath)) {
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
return false;
}
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
size_t contentOpfSize;
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
return false;
}
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
if (!opfParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
return false;
}
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
return false;
}
// 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;
if (!opfParser.tocNcxPath.empty()) {
tocNcxItem = opfParser.tocNcxPath;
}
if (!opfParser.tocNavPath.empty()) {
tocNavItem = opfParser.tocNavPath;
}
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
return true;
}
bool Epub::parseTocNcxFile() const {
// the ncx file should have been specified in the content.opf file
if (tocNcxItem.empty()) {
Serial.println("No ncx file specified");
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
return false;
}
const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true));
if (!ncxData) {
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
FsFile tempNcxFile;
if (!SdMan.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
return false;
}
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
tempNcxFile.close();
if (!SdMan.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
return false;
}
const auto ncxSize = tempNcxFile.size();
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
if (!ncxParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
tempNcxFile.close();
return false;
}
// Parse the Toc contents
tinyxml2::XMLDocument doc;
const auto result = doc.Parse(ncxData);
free(ncxData);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("Error parsing toc %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
if (!ncxBuffer) {
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
tempNcxFile.close();
return false;
}
const auto ncx = doc.FirstChildElement("ncx");
if (!ncx) {
Serial.println("Could not find first child ncx in toc");
return false;
while (tempNcxFile.available()) {
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
if (readSize == 0) break;
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
if (processedSize != readSize) {
Serial.printf("[%lu] [EBP] Could not process all toc ncx data\n", millis());
free(ncxBuffer);
tempNcxFile.close();
return false;
}
}
const auto navMap = ncx->FirstChildElement("navMap");
if (!navMap) {
Serial.println("Could not find navMap child in ncx");
return false;
}
free(ncxBuffer);
tempNcxFile.close();
SdMan.remove(tmpNcxPath.c_str());
recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
return true;
}
void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) {
// Fills toc map
while (element) {
std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
const auto content = element->FirstChildElement("content");
std::string href = contentBasePath + content->Attribute("src");
// split the href on the # to get the href and the anchor
const size_t pos = href.find('#');
std::string anchor;
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
toc.emplace_back(navTitle, href, anchor, 0);
tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint");
if (nestedNavPoint) {
recursivelyParseNavMap(nestedNavPoint);
}
element = element->NextSiblingElement("navPoint");
bool Epub::parseTocNavFile() const {
// the nav file should have been specified in the content.opf file (EPUB 3)
if (tocNavItem.empty()) {
Serial.printf("[%lu] [EBP] No nav file specified\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] Parsing toc nav file: %s\n", millis(), tocNavItem.c_str());
const auto tmpNavPath = getCachePath() + "/toc.nav";
FsFile tempNavFile;
if (!SdMan.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
return false;
}
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
tempNavFile.close();
if (!SdMan.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
return false;
}
const auto navSize = tempNavFile.size();
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
// and the HTMLX nav file will have hrefs relative to itself
const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1);
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
if (!navParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup toc nav parser\n", millis());
return false;
}
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
if (!navBuffer) {
Serial.printf("[%lu] [EBP] Could not allocate memory for toc nav parser\n", millis());
return false;
}
while (tempNavFile.available()) {
const auto readSize = tempNavFile.read(navBuffer, 1024);
const auto processedSize = navParser.write(navBuffer, readSize);
if (processedSize != readSize) {
Serial.printf("[%lu] [EBP] Could not process all toc nav data\n", millis());
free(navBuffer);
tempNavFile.close();
return false;
}
}
free(navBuffer);
tempNavFile.close();
SdMan.remove(tmpNavPath.c_str());
Serial.printf("[%lu] [EBP] Parsed TOC nav items\n", millis());
return true;
}
// load in the meta data for the epub file
bool Epub::load() {
ZipFile zip("/sd" + filepath);
bool Epub::load(const bool buildIfMissing) {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
std::string contentOpfFile;
if (!findContentOpfFile(zip, contentOpfFile)) {
Serial.println("Could not open ePub");
// Initialize spine/TOC cache
bookMetadataCache.reset(new BookMetadataCache(cachePath));
// Try to load existing cache first
if (bookMetadataCache->load()) {
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true;
}
// If we didn't load from cache above and we aren't allowed to build, fail now
if (!buildIfMissing) {
return false;
}
contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1);
// Cache doesn't exist or is invalid, build it
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
setupCacheDir();
if (!parseContentOpf(zip, contentOpfFile)) {
const uint32_t indexingStart = millis();
// Begin building cache - stream entries to disk immediately
if (!bookMetadataCache->beginWrite()) {
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
return false;
}
if (!parseTocNcxFile(zip)) {
// OPF Pass
const uint32_t opfStart = millis();
BookMetadataCache::BookMetadata bookMetadata;
if (!bookMetadataCache->beginContentOpfPass()) {
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
return false;
}
if (!parseContentOpf(bookMetadata)) {
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
return false;
}
if (!bookMetadataCache->endContentOpfPass()) {
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
// TOC Pass - try EPUB 3 nav first, fall back to NCX
const uint32_t tocStart = millis();
if (!bookMetadataCache->beginTocPass()) {
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
return false;
}
bool tocParsed = false;
// Try EPUB 3 nav document first (preferred)
if (!tocNavItem.empty()) {
Serial.printf("[%lu] [EBP] Attempting to parse EPUB 3 nav document\n", millis());
tocParsed = parseTocNavFile();
}
// Fall back to NCX if nav parsing failed or wasn't available
if (!tocParsed && !tocNcxItem.empty()) {
Serial.printf("[%lu] [EBP] Falling back to NCX TOC\n", millis());
tocParsed = parseTocNcxFile();
}
if (!tocParsed) {
Serial.printf("[%lu] [EBP] Warning: Could not parse any TOC format\n", millis());
// Continue anyway - book will work without TOC
}
if (!bookMetadataCache->endTocPass()) {
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
// Close the cache files
if (!bookMetadataCache->endWrite()) {
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
return false;
}
// Build final book.bin
const uint32_t buildStart = millis();
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
if (!bookMetadataCache->cleanupTmpFiles()) {
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
}
// Reload the cache from disk so it's in the correct state
bookMetadataCache.reset(new BookMetadataCache(cachePath));
if (!bookMetadataCache->load()) {
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true;
}
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
bool Epub::clearCache() const {
if (!SdMan.exists(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
return true;
}
if (!SdMan.removeDir(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
return false;
}
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
return true;
}
void Epub::setupCacheDir() const {
if (SD.exists(cachePath.c_str())) {
if (SdMan.exists(cachePath.c_str())) {
return;
}
// Loop over each segment of the cache path and create directories as needed
for (size_t i = 1; i < cachePath.length(); i++) {
if (cachePath[i] == '/') {
SD.mkdir(cachePath.substr(0, i).c_str());
}
}
SD.mkdir(cachePath.c_str());
SdMan.mkdir(cachePath.c_str());
}
const std::string& Epub::getCachePath() const { return cachePath; }
const std::string& Epub::getPath() const { return filepath; }
const std::string& Epub::getTitle() const { return title; }
const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
std::string normalisePath(const std::string& path) {
std::vector<std::string> components;
std::string component;
for (const auto c : path) {
if (c == '/') {
if (!component.empty()) {
if (component == "..") {
if (!components.empty()) {
components.pop_back();
}
} else {
components.push_back(component);
}
component.clear();
}
} else {
component += c;
}
const std::string& Epub::getTitle() const {
static std::string blank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
if (!component.empty()) {
components.push_back(component);
}
std::string result;
for (const auto& c : components) {
if (!result.empty()) {
result += "/";
}
result += c;
}
return result;
return bookMetadataCache->coreMetadata.title;
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, bool trailingNullByte) const {
const ZipFile zip("/sd" + filepath);
const std::string path = normalisePath(itemHref);
const std::string& Epub::getAuthor() const {
static std::string blank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
return bookMetadataCache->coreMetadata.author;
}
const std::string& Epub::getLanguage() const {
static std::string blank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.language;
}
std::string Epub::getCoverBmpPath(bool cropped) const {
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
return cachePath + "/" + coverFileName + ".bmp";
}
bool Epub::generateCoverBmp(bool cropped) const {
// Already generated, return true
if (SdMan.exists(getCoverBmpPath(cropped).c_str())) {
return true;
}
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
return false;
}
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
return false;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit");
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
FsFile coverJpg;
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
readItemContentsToStream(coverImageHref, coverJpg, 1024);
coverJpg.close();
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
FsFile coverBmp;
if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
coverJpg.close();
return false;
}
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
coverJpg.close();
coverBmp.close();
SdMan.remove(coverJpgTempPath.c_str());
if (!success) {
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
SdMan.remove(getCoverBmpPath(cropped).c_str());
}
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
return success;
} else {
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
}
return false;
}
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
bool Epub::generateThumbBmp() const {
// Already generated, return true
if (SdMan.exists(getThumbBmpPath().c_str())) {
return true;
}
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis());
return false;
}
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis());
return false;
}
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis());
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
FsFile coverJpg;
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
readItemContentsToStream(coverImageHref, coverJpg, 1024);
coverJpg.close();
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
return false;
}
FsFile thumbBmp;
if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
coverJpg.close();
return false;
}
// Use smaller target size for Continue Reading card (half of screen: 240x400)
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
constexpr int THUMB_TARGET_WIDTH = 240;
constexpr int THUMB_TARGET_HEIGHT = 400;
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
THUMB_TARGET_HEIGHT);
coverJpg.close();
thumbBmp.close();
SdMan.remove(coverJpgTempPath.c_str());
if (!success) {
Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis());
SdMan.remove(getThumbBmpPath().c_str());
}
Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(),
success ? "yes" : "no");
return success;
} else {
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis());
}
return false;
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
if (itemHref.empty()) {
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
return nullptr;
}
const std::string path = FsHelpers::normalisePath(itemHref);
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
if (!content) {
Serial.printf("Failed to read item %s\n", path.c_str());
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
return nullptr;
}
@@ -322,59 +510,133 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
}
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
const ZipFile zip("/sd" + filepath);
const std::string path = normalisePath(itemHref);
return zip.readFileToStream(path.c_str(), out, chunkSize);
}
int Epub::getSpineItemsCount() const { return spine.size(); }
std::string& Epub::getSpineItem(const int spineIndex) {
if (spineIndex < 0 || spineIndex >= spine.size()) {
Serial.printf("getSpineItem index:%d is out of range\n", spineIndex);
return spine.at(0).second;
if (itemHref.empty()) {
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
return false;
}
return spine.at(spineIndex).second;
const std::string path = FsHelpers::normalisePath(itemHref);
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize);
}
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
if (tocTndex < 0 || tocTndex >= toc.size()) {
Serial.printf("getTocItem index:%d is out of range\n", tocTndex);
return toc.at(0);
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
const std::string path = FsHelpers::normalisePath(itemHref);
return ZipFile(filepath).getInflatedFileSize(path.c_str(), size);
}
int Epub::getSpineItemsCount() const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return 0;
}
return bookMetadataCache->getSpineCount();
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
return {};
}
return toc.at(tocTndex);
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
return bookMetadataCache->getSpineEntry(0);
}
return bookMetadataCache->getSpineEntry(spineIndex);
}
int Epub::getTocItemsCount() const { return toc.size(); }
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
return {};
}
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
return {};
}
return bookMetadataCache->getTocEntry(tocIndex);
}
int Epub::getTocItemsCount() const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return 0;
}
return bookMetadataCache->getTocCount();
}
// work out the section index for a toc index
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
// the toc entry should have an href that matches the spine item
// so we can find the spine index by looking for the href
for (int i = 0; i < spine.size(); i++) {
if (spine[i].second == toc[tocIndex].href) {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
return 0;
}
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
return 0;
}
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
if (spineIndex < 0) {
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
return 0;
}
return spineIndex;
}
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
size_t Epub::getBookSize() const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
return 0;
}
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
}
int Epub::getSpineIndexForTextReference() const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
return 0;
}
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
bookMetadataCache->coreMetadata.coverItemHref.size(),
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
bookMetadataCache->coreMetadata.textReferenceHref.size(),
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
// there was no textReference in epub, so we return 0 (the first chapter)
return 0;
}
// loop through spine items to get the correct index matching the text href
for (size_t i = 0; i < getSpineItemsCount(); i++) {
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
return i;
}
}
Serial.println("Section not found");
// not found - default to the start of the book
// This should not happen, as we checked for empty textReferenceHref earlier
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
return 0;
}
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
// the toc entry should have an href that matches the spine item
// so we can find the toc index by looking for the href
for (int i = 0; i < toc.size(); i++) {
if (toc[i].href == spine[spineIndex].second) {
return i;
}
// 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.0f;
}
Serial.println("TOC item not found");
// not found - default to first item
return 0;
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
return totalProgress / static_cast<float>(bookSize);
}

View File

@@ -1,46 +1,34 @@
#pragma once
#include <HardwareSerial.h>
#include <tinyxml2.h>
#include <Print.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "Epub/BookMetadataCache.h"
class ZipFile;
class EpubTocEntry {
public:
std::string title;
std::string href;
std::string anchor;
int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
};
class Epub {
// the title read from the EPUB meta data
std::string title;
// the cover image
std::string coverImageItem;
// the ncx file
// the ncx file (EPUB 2)
std::string tocNcxItem;
// the nav file (EPUB 3)
std::string tocNavItem;
// where is the EPUBfile?
std::string filepath;
// the spine of the EPUB file
std::vector<std::pair<std::string, std::string>> spine;
// the toc of the EPUB file
std::vector<EpubTocEntry> toc;
// the base path for items in the EPUB file
std::string contentBasePath;
// Uniq cache key based on filepath
std::string cachePath;
// Spine and TOC cache
std::unique_ptr<BookMetadataCache> bookMetadataCache;
// find the path for the content.opf file
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
bool parseTocNcxFile(const ZipFile& zip);
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
bool parseTocNcxFile() const;
bool parseTocNavFile() const;
public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@@ -49,20 +37,31 @@ class Epub {
}
~Epub() = default;
std::string& getBasePath() { return contentBasePath; }
bool load();
void clearCache() const;
bool load(bool buildIfMissing = true);
bool clearCache() const;
void setupCacheDir() const;
const std::string& getCachePath() const;
const std::string& getPath() const;
const std::string& getTitle() const;
const std::string& getCoverImageItem() const;
const std::string& getAuthor() const;
const std::string& getLanguage() const;
std::string getCoverBmpPath(bool cropped = false) const;
bool generateCoverBmp(bool cropped = false) const;
std::string getThumbBmpPath() const;
bool generateThumbBmp() const;
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
std::string& getSpineItem(int spineIndex);
bool getItemSize(const std::string& itemHref, size_t* size) const;
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex);
int getTocItemsCount() const;
int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const;
size_t getCumulativeSpineItemSize(int spineIndex) const;
int getSpineIndexForTextReference() const;
size_t getBookSize() const;
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
};

View File

@@ -0,0 +1,448 @@
#include "BookMetadataCache.h"
#include <HardwareSerial.h>
#include <Serialization.h>
#include <ZipFile.h>
#include <vector>
#include "FsHelpers.h"
namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 5;
constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
} // namespace
/* ============= WRITING / BUILDING FUNCTIONS ================ */
bool BookMetadataCache::beginWrite() {
buildMode = true;
spineCount = 0;
tocCount = 0;
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
return true;
}
bool BookMetadataCache::beginContentOpfPass() {
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
// Open spine file for writing
return SdMan.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
}
bool BookMetadataCache::endContentOpfPass() {
spineFile.close();
return true;
}
bool BookMetadataCache::beginTocPass() {
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
return false;
}
if (!SdMan.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
spineFile.close();
return false;
}
if (spineCount >= LARGE_SPINE_THRESHOLD) {
spineHrefIndex.clear();
spineHrefIndex.reserve(spineCount);
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
auto entry = readSpineEntry(spineFile);
SpineHrefIndexEntry idx;
idx.hrefHash = fnvHash64(entry.href);
idx.hrefLen = static_cast<uint16_t>(entry.href.size());
idx.spineIndex = static_cast<int16_t>(i);
spineHrefIndex.push_back(idx);
}
std::sort(spineHrefIndex.begin(), spineHrefIndex.end(),
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
});
spineFile.seek(0);
useSpineHrefIndex = true;
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
} else {
useSpineHrefIndex = false;
}
return true;
}
bool BookMetadataCache::endTocPass() {
tocFile.close();
spineFile.close();
spineHrefIndex.clear();
spineHrefIndex.shrink_to_fit();
useSpineHrefIndex = false;
return true;
}
bool BookMetadataCache::endWrite() {
if (!buildMode) {
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
return false;
}
buildMode = false;
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
return true;
}
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
// Open all three files, writing to meta, reading from spine and toc
if (!SdMan.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
return false;
}
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
bookFile.close();
return false;
}
if (!SdMan.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
bookFile.close();
spineFile.close();
return false;
}
constexpr uint32_t headerASize =
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
sizeof(uint32_t) * 5;
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
const uint32_t lutOffset = headerASize + metadataSize;
// Header A
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
serialization::writePod(bookFile, lutOffset);
serialization::writePod(bookFile, spineCount);
serialization::writePod(bookFile, tocCount);
// Metadata
serialization::writeString(bookFile, metadata.title);
serialization::writeString(bookFile, metadata.author);
serialization::writeString(bookFile, metadata.language);
serialization::writeString(bookFile, metadata.coverItemHref);
serialization::writeString(bookFile, metadata.textReferenceHref);
// Loop through spine entries, writing LUT positions
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
uint32_t pos = spineFile.position();
auto spineEntry = readSpineEntry(spineFile);
serialization::writePod(bookFile, pos + lutOffset + lutSize);
}
// Loop through toc entries, writing LUT positions
tocFile.seek(0);
for (int i = 0; i < tocCount; i++) {
uint32_t pos = tocFile.position();
auto tocEntry = readTocEntry(tocFile);
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
}
// LUTs complete
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
std::vector<int16_t> spineToTocIndex(spineCount, -1);
tocFile.seek(0);
for (int j = 0; j < tocCount; j++) {
auto tocEntry = readTocEntry(tocFile);
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
}
}
}
ZipFile zip(epubPath);
// Pre-open zip file to speed up size calculations
if (!zip.open()) {
Serial.printf("[%lu] [BMC] Could not open EPUB zip for size calculations\n", millis());
bookFile.close();
spineFile.close();
tocFile.close();
return false;
}
// NOTE: We intentionally skip calling loadAllFileStatSlims() here.
// For large EPUBs (2000+ chapters), pre-loading all ZIP central directory entries
// into memory causes OOM crashes on ESP32-C3's limited ~380KB RAM.
// Instead, for large books we use a one-pass batch lookup that scans the ZIP
// central directory once and matches against spine targets using hash comparison.
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
std::vector<uint32_t> spineSizes;
bool useBatchSizes = false;
if (spineCount >= LARGE_SPINE_THRESHOLD) {
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
std::vector<ZipFile::SizeTarget> targets;
targets.reserve(spineCount);
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
auto entry = readSpineEntry(spineFile);
std::string path = FsHelpers::normalisePath(entry.href);
ZipFile::SizeTarget t;
t.hash = ZipFile::fnvHash64(path.c_str(), path.size());
t.len = static_cast<uint16_t>(path.size());
t.index = static_cast<uint16_t>(i);
targets.push_back(t);
}
std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) {
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
});
spineSizes.resize(spineCount, 0);
int matched = zip.fillUncompressedSizes(targets, spineSizes);
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
targets.clear();
targets.shrink_to_fit();
useBatchSizes = true;
}
uint32_t cumSize = 0;
spineFile.seek(0);
int lastSpineTocIndex = -1;
for (int i = 0; i < spineCount; i++) {
auto spineEntry = readSpineEntry(spineFile);
spineEntry.tocIndex = spineToTocIndex[i];
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
// Logging here is for debugging
if (spineEntry.tocIndex == -1) {
Serial.printf(
"[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s, using title from last section\n",
millis(), i, spineEntry.href.c_str());
spineEntry.tocIndex = lastSpineTocIndex;
}
lastSpineTocIndex = spineEntry.tocIndex;
size_t itemSize = 0;
if (useBatchSizes) {
itemSize = spineSizes[i];
if (itemSize == 0) {
const std::string path = FsHelpers::normalisePath(spineEntry.href);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
}
}
} else {
const std::string path = FsHelpers::normalisePath(spineEntry.href);
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
}
}
cumSize += itemSize;
spineEntry.cumulativeSize = cumSize;
// Write out spine data to book.bin
writeSpineEntry(bookFile, spineEntry);
}
// Close opened zip file
zip.close();
// Loop through toc entries from toc file writing to book.bin
tocFile.seek(0);
for (int i = 0; i < tocCount; i++) {
auto tocEntry = readTocEntry(tocFile);
writeTocEntry(bookFile, tocEntry);
}
bookFile.close();
spineFile.close();
tocFile.close();
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
return true;
}
bool BookMetadataCache::cleanupTmpFiles() const {
if (SdMan.exists((cachePath + tmpSpineBinFile).c_str())) {
SdMan.remove((cachePath + tmpSpineBinFile).c_str());
}
if (SdMan.exists((cachePath + tmpTocBinFile).c_str())) {
SdMan.remove((cachePath + tmpTocBinFile).c_str());
}
return true;
}
uint32_t BookMetadataCache::writeSpineEntry(FsFile& file, const SpineEntry& entry) const {
const uint32_t pos = file.position();
serialization::writeString(file, entry.href);
serialization::writePod(file, entry.cumulativeSize);
serialization::writePod(file, entry.tocIndex);
return pos;
}
uint32_t BookMetadataCache::writeTocEntry(FsFile& file, const TocEntry& entry) const {
const uint32_t pos = file.position();
serialization::writeString(file, entry.title);
serialization::writeString(file, entry.href);
serialization::writeString(file, entry.anchor);
serialization::writePod(file, entry.level);
serialization::writePod(file, entry.spineIndex);
return pos;
}
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
// this is because in this function we're marking positions of the items
void BookMetadataCache::createSpineEntry(const std::string& href) {
if (!buildMode || !spineFile) {
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
return;
}
const SpineEntry entry(href, 0, -1);
writeSpineEntry(spineFile, entry);
spineCount++;
}
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
const uint8_t level) {
if (!buildMode || !tocFile || !spineFile) {
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
return;
}
int16_t spineIndex = -1;
if (useSpineHrefIndex) {
uint64_t targetHash = fnvHash64(href);
uint16_t targetLen = static_cast<uint16_t>(href.size());
auto it =
std::lower_bound(spineHrefIndex.begin(), spineHrefIndex.end(), SpineHrefIndexEntry{targetHash, targetLen, 0},
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
});
while (it != spineHrefIndex.end() && it->hrefHash == targetHash && it->hrefLen == targetLen) {
spineIndex = it->spineIndex;
break;
}
if (spineIndex == -1) {
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
}
} else {
spineFile.seek(0);
for (int i = 0; i < spineCount; i++) {
auto spineEntry = readSpineEntry(spineFile);
if (spineEntry.href == href) {
spineIndex = static_cast<int16_t>(i);
break;
}
}
if (spineIndex == -1) {
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
}
}
const TocEntry entry(title, href, anchor, level, spineIndex);
writeTocEntry(tocFile, entry);
tocCount++;
}
/* ============= READING / LOADING FUNCTIONS ================ */
bool BookMetadataCache::load() {
if (!SdMan.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
return false;
}
uint8_t version;
serialization::readPod(bookFile, version);
if (version != BOOK_CACHE_VERSION) {
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
bookFile.close();
return false;
}
serialization::readPod(bookFile, lutOffset);
serialization::readPod(bookFile, spineCount);
serialization::readPod(bookFile, tocCount);
serialization::readString(bookFile, coreMetadata.title);
serialization::readString(bookFile, coreMetadata.author);
serialization::readString(bookFile, coreMetadata.language);
serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref);
loaded = true;
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
return true;
}
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
if (!loaded) {
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
return {};
}
if (index < 0 || index >= static_cast<int>(spineCount)) {
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
return {};
}
// Seek to spine LUT item, read from LUT and get out data
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
uint32_t spineEntryPos;
serialization::readPod(bookFile, spineEntryPos);
bookFile.seek(spineEntryPos);
return readSpineEntry(bookFile);
}
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
if (!loaded) {
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
return {};
}
if (index < 0 || index >= static_cast<int>(tocCount)) {
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
return {};
}
// Seek to TOC LUT item, read from LUT and get out data
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
uint32_t tocEntryPos;
serialization::readPod(bookFile, tocEntryPos);
bookFile.seek(tocEntryPos);
return readTocEntry(bookFile);
}
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(FsFile& file) const {
SpineEntry entry;
serialization::readString(file, entry.href);
serialization::readPod(file, entry.cumulativeSize);
serialization::readPod(file, entry.tocIndex);
return entry;
}
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(FsFile& file) const {
TocEntry entry;
serialization::readString(file, entry.title);
serialization::readString(file, entry.href);
serialization::readString(file, entry.anchor);
serialization::readPod(file, entry.level);
serialization::readPod(file, entry.spineIndex);
return entry;
}

View File

@@ -0,0 +1,112 @@
#pragma once
#include <SDCardManager.h>
#include <algorithm>
#include <string>
#include <vector>
class BookMetadataCache {
public:
struct BookMetadata {
std::string title;
std::string author;
std::string language;
std::string coverItemHref;
std::string textReferenceHref;
};
struct SpineEntry {
std::string href;
size_t cumulativeSize;
int16_t tocIndex;
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
SpineEntry(std::string href, const size_t cumulativeSize, const int16_t tocIndex)
: href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {}
};
struct TocEntry {
std::string title;
std::string href;
std::string anchor;
uint8_t level;
int16_t spineIndex;
TocEntry() : level(0), spineIndex(-1) {}
TocEntry(std::string title, std::string href, std::string anchor, const uint8_t level, const int16_t spineIndex)
: title(std::move(title)),
href(std::move(href)),
anchor(std::move(anchor)),
level(level),
spineIndex(spineIndex) {}
};
private:
std::string cachePath;
size_t lutOffset;
uint16_t spineCount;
uint16_t tocCount;
bool loaded;
bool buildMode;
FsFile bookFile;
// Temp file handles during build
FsFile spineFile;
FsFile tocFile;
// Index for fast href→spineIndex lookup (used only for large EPUBs)
struct SpineHrefIndexEntry {
uint64_t hrefHash; // FNV-1a 64-bit hash
uint16_t hrefLen; // length for collision reduction
int16_t spineIndex;
};
std::vector<SpineHrefIndexEntry> spineHrefIndex;
bool useSpineHrefIndex = false;
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
// FNV-1a 64-bit hash function
static uint64_t fnvHash64(const std::string& s) {
uint64_t hash = 14695981039346656037ull;
for (char c : s) {
hash ^= static_cast<uint8_t>(c);
hash *= 1099511628211ull;
}
return hash;
}
uint32_t writeSpineEntry(FsFile& file, const SpineEntry& entry) const;
uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const;
SpineEntry readSpineEntry(FsFile& file) const;
TocEntry readTocEntry(FsFile& file) const;
public:
BookMetadata coreMetadata;
explicit BookMetadataCache(std::string cachePath)
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
~BookMetadataCache() = default;
// Building phase (stream to disk immediately)
bool beginWrite();
bool beginContentOpfPass();
void createSpineEntry(const std::string& href);
bool endContentOpfPass();
bool beginTocPass();
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
bool endTocPass();
bool endWrite();
bool cleanupTmpFiles() const;
// Post-processing to update mappings and sizes
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
// Reading phase (read mode)
bool load();
SpineEntry getSpineEntry(int index);
TocEntry getTocEntry(int index);
int getSpineCount() const { return spineCount; }
int getTocCount() const { return tocCount; }
bool isLoaded() const { return loaded; }
};

View File

@@ -1,293 +0,0 @@
#include "EpubHtmlParserSlim.h"
#include <EpdRenderer.h>
#include <HardwareSerial.h>
#include <expat.h>
#include "Page.h"
#include "htmlEntities.h"
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
const char* BLOCK_TAGS[] = {"p", "li", "div", "br"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
const char* BOLD_TAGS[] = {"b"};
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
const char* ITALIC_TAGS[] = {"i"};
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"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n'; }
// given the start and end of a tag, check to see if it matches a known tag
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
for (int i = 0; i < possible_tag_count; i++) {
if (strcmp(tag_name, possible_tags[i]) == 0) {
return true;
}
}
return false;
}
// start a new text block if needed
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) {
currentTextBlock->setStyle(style);
return;
}
currentTextBlock->finish();
makePages();
delete currentTextBlock;
}
currentTextBlock = new TextBlock(style);
}
void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
(void)atts;
// Middle of skip
if (self->skipUntilDepth < self->depth) {
self->depth += 1;
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// const char* src = element.Attribute("src");
// if (src) {
// // don't leave an empty text block in the list
// // const BLOCK_STYLE style = currentTextBlock->get_style();
// if (currentTextBlock->isEmpty()) {
// delete currentTextBlock;
// currentTextBlock = nullptr;
// }
// // TODO: Fix this
// // blocks.push_back(new ImageBlock(m_base_path + src));
// // start a new text block - with the same style as before
// // startNewTextBlock(style);
// } else {
// // ESP_LOGE(TAG, "Could not find src attribute");
// }
// start skip
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
// start skip
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) {
self->startNewTextBlock(self->currentTextBlock->getStyle());
} else {
self->startNewTextBlock(JUSTIFIED);
}
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
}
self->depth += 1;
}
void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
// Middle of skip
if (self->skipUntilDepth < self->depth) {
return;
}
for (int i = 0; i < len; i++) {
if (isWhitespace(s[i])) {
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
if (self->partWordBufferIndex > 0) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0;
}
// Skip the whitespace char
continue;
}
// If we're about to run out of space, then cut the word off and start a new one
if (self->partWordBufferIndex >= PART_WORD_BUFFER_SIZE - 2) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0;
}
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
}
}
void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
(void)name;
if (self->partWordBufferIndex > 0) {
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
// We don't want to flush out content when closing inline tags like <span>.
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
// text styling needs to be overhauled to fix it.
const bool shouldBreakText =
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
if (shouldBreakText) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0;
}
}
self->depth -= 1;
// Leaving skip
if (self->skipUntilDepth == self->depth) {
self->skipUntilDepth = INT_MAX;
}
// Leaving bold
if (self->boldUntilDepth == self->depth) {
self->boldUntilDepth = INT_MAX;
}
// Leaving italic
if (self->italicUntilDepth == self->depth) {
self->italicUntilDepth = INT_MAX;
}
}
bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr);
int done;
if (!parser) {
Serial.println("Couldn't allocate memory for parser");
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
FILE* file = fopen(filepath, "r");
if (!file) {
Serial.printf("Couldn't open file %s\n", filepath);
XML_ParserFree(parser);
return false;
}
do {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.println("Couldn't allocate memory for buffer");
XML_ParserFree(parser);
fclose(file);
return false;
}
const size_t len = fread(buf, 1, 1024, file);
if (ferror(file)) {
Serial.println("Read error");
XML_ParserFree(parser);
fclose(file);
return false;
}
done = feof(file);
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
Serial.printf("Parse error at line %lu:\n%s\n", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
XML_ParserFree(parser);
fclose(file);
return false;
}
} while (!done);
XML_ParserFree(parser);
fclose(file);
// Process last page if there is still text
if (currentTextBlock) {
makePages();
completePageFn(currentPage);
currentPage = nullptr;
delete currentTextBlock;
currentTextBlock = nullptr;
}
return true;
}
void EpubHtmlParserSlim::makePages() {
if (!currentTextBlock) {
Serial.println("!! No text block to make pages for !!");
return;
}
if (!currentPage) {
currentPage = new Page();
}
const int lineHeight = renderer.getLineHeight();
const int pageHeight = renderer.getPageHeight();
// Long running task, make sure to let other things happen
vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) {
const auto lines = currentTextBlock->splitIntoLines(renderer);
for (const auto line : lines) {
if (currentPage->nextY + lineHeight > pageHeight) {
completePageFn(currentPage);
currentPage = new Page();
}
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
currentPage->nextY += lineHeight;
}
// add some extra line between blocks
currentPage->nextY += lineHeight / 2;
}
// TODO: Image block support
// if (block->getType() == BlockType::IMAGE_BLOCK) {
// ImageBlock *imageBlock = (ImageBlock *)block;
// if (y + imageBlock->height > page_height) {
// pages.push_back(new Page());
// y = 0;
// }
// pages.back()->elements.push_back(new PageImage(imageBlock, y));
// y += imageBlock->height;
// }
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include <expat.h>
#include <limits.h>
#include <functional>
#include "blocks/TextBlock.h"
class Page;
class EpdRenderer;
#define PART_WORD_BUFFER_SIZE 200
class EpubHtmlParserSlim {
const char* filepath;
EpdRenderer& renderer;
std::function<void(Page*)> completePageFn;
int depth = 0;
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX;
// If we encounter words longer than this, but this is pretty large
char partWordBuffer[PART_WORD_BUFFER_SIZE] = {};
int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr;
Page* currentPage = nullptr;
void startNewTextBlock(BLOCK_STYLE style);
void makePages();
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit EpubHtmlParserSlim(const char* filepath, EpdRenderer& renderer,
const std::function<void(Page*)>& completePageFn)
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
~EpubHtmlParserSlim() = default;
bool parseAndBuildPages();
};

View File

@@ -3,61 +3,65 @@
#include <HardwareSerial.h>
#include <Serialization.h>
void PageLine::render(EpdRenderer& renderer) { block->render(renderer, 0, yPos); }
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
}
void PageLine::serialize(std::ostream& os) {
serialization::writePod(os, yPos);
bool PageLine::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
// serialize TextBlock pointed to by PageLine
block->serialize(os);
return block->serialize(file);
}
PageLine* PageLine::deserialize(std::istream& is) {
int32_t yPos;
serialization::readPod(is, yPos);
std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
int16_t xPos;
int16_t yPos;
serialization::readPod(file, xPos);
serialization::readPod(file, yPos);
const auto tb = TextBlock::deserialize(is);
return new PageLine(tb, yPos);
auto tb = TextBlock::deserialize(file);
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
}
void Page::render(EpdRenderer& renderer) const {
const auto start = millis();
for (const auto element : elements) {
element->render(renderer);
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset);
}
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
}
void Page::serialize(std::ostream& os) const {
serialization::writePod(os, nextY);
bool Page::serialize(FsFile& file) const {
const uint16_t count = elements.size();
serialization::writePod(file, count);
const uint32_t count = elements.size();
serialization::writePod(os, count);
for (auto* el : elements) {
for (const auto& el : elements) {
// Only PageLine exists currently
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
static_cast<PageLine*>(el)->serialize(os);
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
if (!el->serialize(file)) {
return false;
}
}
return true;
}
Page* Page::deserialize(std::istream& is) {
auto* page = new Page();
std::unique_ptr<Page> Page::deserialize(FsFile& file) {
auto page = std::unique_ptr<Page>(new Page());
serialization::readPod(is, page->nextY);
uint16_t count;
serialization::readPod(file, count);
uint32_t count;
serialization::readPod(is, count);
for (uint32_t i = 0; i < count; i++) {
for (uint16_t i = 0; i < count; i++) {
uint8_t tag;
serialization::readPod(is, tag);
serialization::readPod(file, tag);
if (tag == TAG_PageLine) {
auto* pl = PageLine::deserialize(is);
page->elements.push_back(pl);
auto pl = PageLine::deserialize(file);
page->elements.push_back(std::move(pl));
} else {
throw std::runtime_error("Unknown PageElement tag");
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
return nullptr;
}
}

View File

@@ -1,4 +1,9 @@
#pragma once
#include <SdFat.h>
#include <utility>
#include <vector>
#include "blocks/TextBlock.h"
enum PageElementTag : uint8_t {
@@ -8,36 +13,31 @@ enum PageElementTag : uint8_t {
// represents something that has been added to a page
class PageElement {
public:
int yPos;
explicit PageElement(const int yPos) : yPos(yPos) {}
int16_t xPos;
int16_t yPos;
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default;
virtual void render(EpdRenderer& renderer) = 0;
virtual void serialize(std::ostream& os) = 0;
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
virtual bool serialize(FsFile& file) = 0;
};
// a line from a block element
class PageLine final : public PageElement {
const TextBlock* block;
std::shared_ptr<TextBlock> block;
public:
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
~PageLine() override { delete block; }
void render(EpdRenderer& renderer) override;
void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is);
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
static std::unique_ptr<PageLine> deserialize(FsFile& file);
};
class Page {
public:
int nextY = 0;
// the list of block index and line numbers on this page
std::vector<PageElement*> elements;
void render(EpdRenderer& renderer) const;
~Page() {
for (const auto element : elements) {
delete element;
}
}
void serialize(std::ostream& os) const;
static Page* deserialize(std::istream& is);
std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file);
};

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