7 Commits
0.3.0 ... 0.4.0

Author SHA1 Message Date
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
25 changed files with 675 additions and 364 deletions

2
.clangd Normal file
View File

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

View File

@@ -36,7 +36,30 @@ This project is **not affiliated with Xteink**; it's built as a community projec
- [ ] WiFi connectivity - [ ] WiFi connectivity
- [ ] BLE connectivity - [ ] BLE connectivity
## Getting Started ## 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 ### Prerequisites
@@ -58,24 +81,12 @@ git submodule update --init --recursive
### Flashing your device ### Flashing your device
#### Command line
Connect your Xteink X4 to your computer via USB-C and run the following command. Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh ```sh
pio run --target upload 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 ## Internals
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only

View File

@@ -0,0 +1,184 @@
/**
* generated by fontconvert.py
* name: pixelarial14
* size: 8
* mode: 1-bit
*/
#pragma once
#include "EpdFontData.h"
static const uint8_t pixelarial14Bitmaps[1145] = {
0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x1C, 0x63, 0x8C, 0xF7, 0x98, 0xCF, 0xFF, 0xFF, 0xDC, 0xE7, 0xFE, 0xFF,
0xFF, 0xFB, 0x9C, 0x63, 0x0C, 0x60, 0x30, 0xF3, 0xFF, 0xBF, 0x1F, 0x9F, 0x9B, 0x37, 0xEF, 0xFB, 0xE3, 0x00, 0x70,
0x67, 0xCF, 0x37, 0x61, 0xBB, 0x0D, 0xF0, 0x7D, 0x81, 0xDD, 0xC0, 0xDF, 0x07, 0x98, 0xFC, 0xC7, 0x66, 0x7B, 0xF3,
0x07, 0x00, 0x3E, 0x0F, 0xE1, 0x8C, 0x31, 0x86, 0x60, 0xFC, 0x1F, 0x07, 0xE4, 0xC7, 0x98, 0xF3, 0x0E, 0x7F, 0xF7,
0xE6, 0xFF, 0xF0, 0x37, 0x66, 0xCC, 0xCC, 0xCC, 0xCC, 0xC6, 0x67, 0x30, 0xCE, 0x66, 0x33, 0x33, 0x33, 0x33, 0x36,
0x6E, 0xC0, 0x6F, 0xF6, 0xFF, 0x08, 0x0E, 0x07, 0x03, 0x8F, 0xFF, 0xFC, 0x70, 0x38, 0x1C, 0x0E, 0x00, 0xFF, 0xC0,
0xFB, 0xFF, 0x80, 0xF0, 0x1C, 0x73, 0xCC, 0x30, 0xC7, 0x18, 0x61, 0x8E, 0x30, 0xC0, 0x7E, 0xFF, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x7E, 0x37, 0xFF, 0x33, 0x33, 0x33, 0x33, 0x30, 0x7E, 0xFF, 0xC3, 0xC3,
0x03, 0x03, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xFF, 0xFF, 0x7E, 0xFF, 0xC3, 0xC3, 0x03, 0x3F, 0x3F, 0x03, 0x03, 0xC3,
0xC3, 0xFF, 0x7E, 0x03, 0x03, 0x83, 0xC3, 0xE3, 0x31, 0x99, 0xCD, 0xC6, 0xC3, 0x7F, 0xFF, 0xE0, 0x60, 0x30, 0x7F,
0x7F, 0xE0, 0xC0, 0xFE, 0xFF, 0xC3, 0x03, 0x03, 0xC3, 0xC7, 0xFE, 0x7E, 0x7E, 0xFF, 0xC3, 0xC0, 0xC0, 0xFE, 0xFF,
0xE3, 0xC3, 0xC3, 0xC3, 0xFF, 0x7E, 0xFF, 0xFF, 0x07, 0x06, 0x06, 0x1E, 0x1C, 0x1C, 0x1C, 0x38, 0x30, 0x30, 0x30,
0x7E, 0xFF, 0xC3, 0xC3, 0xC3, 0xFF, 0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x7E, 0x7E, 0xFF, 0xC3, 0xC3, 0xC3, 0xC7,
0xFF, 0x7F, 0x03, 0x03, 0xC7, 0xFE, 0x7C, 0xF0, 0x00, 0x3C, 0xF0, 0x00, 0x3F, 0xF0, 0x03, 0x03, 0x1E, 0x7E, 0xF0,
0xF0, 0x70, 0x7E, 0x1F, 0x03, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xC0, 0xC0, 0x70, 0x7E, 0x1F, 0x0F, 0x1E, 0x7E,
0xF0, 0xC0, 0x7E, 0xFF, 0xC3, 0xC3, 0x03, 0x07, 0x1E, 0x1C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3F, 0x87, 0xFE, 0xFF,
0x7D, 0xFB, 0xF1, 0xBF, 0x1B, 0xF3, 0xBF, 0xFF, 0xDF, 0xE6, 0x00, 0x7F, 0x03, 0xF0, 0x06, 0x01, 0xF0, 0x1F, 0x01,
0xF0, 0x1F, 0x03, 0xB8, 0x31, 0x87, 0xFC, 0x7F, 0xE7, 0xFE, 0xE0, 0x7C, 0x03, 0xC0, 0x30, 0xFF, 0x7F, 0xF0, 0x78,
0x3C, 0x1F, 0xFF, 0xFF, 0x83, 0xC1, 0xE0, 0xF0, 0x7F, 0xFF, 0xF0, 0x3F, 0x0F, 0xF3, 0x87, 0xE0, 0x3C, 0x01, 0x80,
0x30, 0x06, 0x00, 0xC0, 0x18, 0x0F, 0x87, 0xBF, 0xC3, 0xF0, 0xFF, 0x1F, 0xF3, 0x07, 0xE0, 0x3C, 0x07, 0x80, 0xF0,
0x1E, 0x03, 0xC0, 0x78, 0x0F, 0x07, 0xFF, 0xCF, 0xF0, 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0xFF, 0xFF, 0x80, 0xC0,
0x60, 0x30, 0x1F, 0xFF, 0xF8, 0xFF, 0xFF, 0xF0, 0x18, 0x0C, 0x07, 0xFB, 0xFD, 0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C,
0x00, 0x3F, 0x87, 0xFE, 0xE0, 0x7C, 0x03, 0xC0, 0x0C, 0x00, 0xC1, 0xFC, 0x1F, 0xC0, 0x3C, 0x03, 0xE0, 0x77, 0xFE,
0x3F, 0x80, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1F, 0xFF, 0xFF, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x18, 0xFF, 0xFF,
0xFF, 0xC0, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC1, 0x83, 0x07, 0x8F, 0x1F, 0xF7, 0xC0, 0xC0, 0x78, 0x3F, 0x0E, 0x61,
0x8C, 0x61, 0xBC, 0x3F, 0x87, 0xB8, 0xE3, 0x1C, 0x63, 0x0E, 0x60, 0xFC, 0x06, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06,
0x03, 0x01, 0x80, 0xC0, 0x60, 0x30, 0x1F, 0xFF, 0xF8, 0xC0, 0x3E, 0x07, 0xE0, 0x7E, 0x07, 0xF1, 0xBF, 0x1B, 0xFB,
0xBD, 0xF3, 0xDF, 0x3D, 0xF3, 0xDF, 0x3C, 0x63, 0xC6, 0x30, 0xC1, 0xF0, 0xFC, 0x7E, 0x3F, 0x1F, 0xEF, 0x77, 0xBB,
0xC7, 0xE3, 0xF1, 0xF8, 0x7C, 0x18, 0x3F, 0x87, 0xFE, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0,
0x3C, 0x03, 0xE0, 0x77, 0xFE, 0x3F, 0x80, 0xFF, 0x7F, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0xFF, 0xFE, 0xC0, 0x60, 0x30,
0x18, 0x0C, 0x00, 0x3F, 0x87, 0xFE, 0xE0, 0x7C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x37, 0xE3,
0xE7, 0xFF, 0x3F, 0x70, 0xFF, 0x9F, 0xFF, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xFF, 0xFF, 0xFC, 0xC6, 0x18, 0xE3, 0x0E,
0x60, 0xFC, 0x06, 0x7F, 0x7F, 0xF0, 0x78, 0x3C, 0x07, 0xE1, 0xFE, 0x0F, 0x01, 0xE0, 0xF0, 0x7F, 0xF7, 0xF0, 0xFF,
0xFF, 0xC7, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xC1, 0xE0, 0xF0, 0x78, 0x3C,
0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF8, 0xEF, 0xE3, 0xE0, 0xC0, 0x3C, 0x03, 0xE0, 0x76, 0x06, 0x60, 0x67, 0x1E,
0x31, 0x83, 0xB8, 0x1F, 0x01, 0xF0, 0x1F, 0x00, 0x60, 0x06, 0x00, 0xC1, 0x81, 0xE1, 0xF0, 0xF8, 0xD8, 0xEC, 0x6C,
0x66, 0x36, 0x33, 0x1B, 0x19, 0xDD, 0xFC, 0x6C, 0x7C, 0x36, 0x3E, 0x1B, 0x1F, 0x0F, 0x8F, 0x03, 0x83, 0x01, 0xC1,
0x80, 0xC0, 0x7C, 0x3D, 0x86, 0x30, 0xC3, 0x30, 0x7E, 0x07, 0x80, 0xF8, 0x33, 0x0E, 0x71, 0x86, 0x70, 0xFC, 0x06,
0xC0, 0x3E, 0x07, 0x71, 0xE3, 0x18, 0x31, 0x83, 0xF8, 0x1F, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06,
0x00, 0xFF, 0xFF, 0xFC, 0x0E, 0x01, 0x80, 0x30, 0x0E, 0x07, 0x80, 0xE0, 0x30, 0x06, 0x01, 0xC0, 0x7F, 0xFF, 0xFE,
0xFF, 0x6D, 0xB6, 0xDB, 0x6D, 0xB7, 0xE0, 0xC3, 0x0E, 0x18, 0x61, 0x87, 0x0C, 0x30, 0xC3, 0x87, 0x1C, 0xFD, 0xB6,
0xDB, 0x6D, 0xB6, 0xDF, 0xE0, 0x30, 0xF1, 0xF3, 0xE7, 0xDD, 0xF1, 0x80, 0xFF, 0xFF, 0xFC, 0xCE, 0x73, 0x00, 0x7E,
0x7E, 0xC3, 0xC3, 0x3F, 0x7F, 0x63, 0xE3, 0xC7, 0xFF, 0x7F, 0xC0, 0xC0, 0xFE, 0xFE, 0xE3, 0xE3, 0xC3, 0xC3, 0xC3,
0xC3, 0xE3, 0xFF, 0xFE, 0x78, 0xFB, 0x1E, 0x3C, 0x18, 0x30, 0x60, 0xC7, 0xFD, 0xF0, 0x03, 0x03, 0x7B, 0x7F, 0xC7,
0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFF, 0x7F, 0x7E, 0x7E, 0xC3, 0xC3, 0xFF, 0xFF, 0xC0, 0xC0, 0xC3, 0xFF, 0x7E,
0x3D, 0xEF, 0xBF, 0x61, 0x86, 0x18, 0x61, 0x86, 0x18, 0x60, 0x7B, 0x7F, 0xC7, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7,
0xFF, 0x7F, 0x03, 0xC3, 0xFF, 0x7E, 0xC0, 0xC0, 0xFE, 0xFE, 0xE3, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xFF, 0xFF, 0xFF, 0xC0, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0xE0, 0xC0, 0xC0, 0xC3, 0xC3, 0xC6, 0xDE,
0xFC, 0xF8, 0xFC, 0xEE, 0xC6, 0xC7, 0xC3, 0xFF, 0xFF, 0xFF, 0xC0, 0xF9, 0xEF, 0xDE, 0xE7, 0x3E, 0x73, 0xC6, 0x3C,
0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x3C, 0x63, 0xC6, 0x30, 0xFE, 0xFE, 0xE3, 0xE3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0x7E, 0x7E, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF, 0x7E, 0xFE, 0xFE, 0xE3, 0xE3, 0xC3, 0xC3, 0xC3,
0xC3, 0xE3, 0xFF, 0xFE, 0xC0, 0xC0, 0xC0, 0xC0, 0x7B, 0x7F, 0xC7, 0xC7, 0xC3, 0xC3, 0xC3, 0xC3, 0xC7, 0xFF, 0x7F,
0x03, 0x03, 0x03, 0x03, 0xFB, 0xFE, 0x38, 0xC3, 0x0C, 0x30, 0xC3, 0x0C, 0x00, 0x78, 0xFB, 0x1E, 0x3F, 0x8F, 0x81,
0x83, 0xC7, 0xFD, 0xF0, 0x61, 0x8F, 0xBF, 0x61, 0x86, 0x18, 0x61, 0x86, 0x1E, 0x7C, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3,
0xC3, 0xC3, 0xC3, 0xC7, 0xFF, 0x7F, 0xC1, 0xE0, 0xF0, 0x7C, 0x76, 0x33, 0xB8, 0xD8, 0x6C, 0x3E, 0x0E, 0x07, 0x00,
0xC6, 0x3C, 0x63, 0xC6, 0x3F, 0xF7, 0x7F, 0x67, 0xF6, 0x7F, 0x67, 0xF6, 0x7B, 0xE3, 0x18, 0x31, 0x80, 0xC3, 0xC3,
0x66, 0x66, 0x7E, 0x3C, 0x3C, 0x7E, 0x66, 0xE7, 0xC3, 0xC1, 0xE0, 0xF0, 0x7C, 0x76, 0x33, 0xB8, 0xD8, 0x6C, 0x36,
0x1F, 0x07, 0x03, 0x81, 0xC3, 0xE1, 0xC0, 0xFF, 0xFF, 0x06, 0x1E, 0x1C, 0x1C, 0x30, 0x30, 0x70, 0xFF, 0xFF, 0x37,
0x66, 0x66, 0x6E, 0xCE, 0x66, 0x66, 0x67, 0x30, 0xFF, 0xFF, 0xFF, 0xC0, 0xCE, 0x66, 0x66, 0x67, 0x37, 0x66, 0x66,
0x6E, 0xC0, 0xC3, 0x9B, 0xFF, 0xF9, 0xB8, 0x30, 0xDB, 0x66, 0xC0, 0x6D, 0xBD, 0x80, 0x7F, 0xEF, 0x3C, 0xF3, 0xC0,
0x7D, 0xF7, 0xDF, 0xF3, 0xC0,
};
static const EpdGlyph pixelarial14Glyphs[] = {
{0, 0, 4, 0, 0, 0, 0}, //
{2, 13, 3, 0, 13, 4, 0}, // !
{4, 6, 5, 0, 13, 3, 4}, // "
{11, 13, 12, 0, 13, 18, 7}, // #
{7, 13, 8, 0, 13, 12, 25}, // $
{13, 13, 14, 0, 13, 22, 37}, // %
{11, 13, 12, 0, 13, 18, 59}, // &
{2, 6, 3, 0, 13, 2, 77}, // '
{4, 17, 5, 0, 13, 9, 79}, // (
{4, 17, 5, 0, 13, 9, 88}, // )
{4, 6, 5, 0, 13, 3, 97}, // *
{9, 10, 10, 0, 11, 12, 100}, // +
{2, 5, 3, 0, 2, 2, 112}, // ,
{6, 3, 6, 0, 6, 3, 114}, // -
{2, 2, 3, 0, 2, 1, 117}, // .
{6, 13, 6, 0, 13, 10, 118}, // /
{8, 13, 9, 0, 13, 13, 128}, // 0
{4, 13, 5, 0, 13, 7, 141}, // 1
{8, 13, 9, 0, 13, 13, 148}, // 2
{8, 13, 9, 0, 13, 13, 161}, // 3
{9, 13, 10, 0, 13, 15, 174}, // 4
{8, 13, 9, 0, 13, 13, 189}, // 5
{8, 13, 9, 0, 13, 13, 202}, // 6
{8, 13, 9, 0, 13, 13, 215}, // 7
{8, 13, 9, 0, 13, 13, 228}, // 8
{8, 13, 9, 0, 13, 13, 241}, // 9
{2, 11, 3, 0, 11, 3, 254}, // :
{2, 14, 3, 0, 11, 4, 257}, // ;
{8, 10, 9, 0, 11, 10, 261}, // <
{8, 6, 9, 0, 9, 6, 271}, // =
{8, 10, 9, 0, 11, 10, 277}, // >
{8, 13, 9, 0, 13, 13, 287}, // ?
{12, 12, 13, 0, 9, 18, 300}, // @
{12, 13, 13, 0, 13, 20, 318}, // A
{9, 13, 10, 0, 13, 15, 338}, // B
{11, 13, 12, 0, 13, 18, 353}, // C
{11, 13, 12, 0, 13, 18, 371}, // D
{9, 13, 10, 0, 13, 15, 389}, // E
{9, 13, 10, 0, 13, 15, 404}, // F
{12, 13, 13, 0, 13, 20, 419}, // G
{9, 13, 10, 0, 13, 15, 439}, // H
{2, 13, 3, 0, 13, 4, 454}, // I
{7, 13, 8, 0, 13, 12, 458}, // J
{11, 13, 12, 0, 13, 18, 470}, // K
{9, 13, 10, 0, 13, 15, 488}, // L
{12, 13, 13, 0, 13, 20, 503}, // M
{9, 13, 10, 0, 13, 15, 523}, // N
{12, 13, 13, 0, 13, 20, 538}, // O
{9, 13, 10, 0, 13, 15, 558}, // P
{12, 13, 13, 0, 13, 20, 573}, // Q
{11, 13, 12, 0, 13, 18, 593}, // R
{9, 13, 10, 0, 13, 15, 611}, // S
{9, 13, 10, 0, 13, 15, 626}, // T
{9, 13, 10, 0, 13, 15, 641}, // U
{12, 13, 13, 0, 13, 20, 656}, // V
{17, 13, 18, 0, 13, 28, 676}, // W
{11, 13, 12, 0, 13, 18, 704}, // X
{12, 13, 13, 0, 13, 20, 722}, // Y
{11, 13, 12, 0, 13, 18, 742}, // Z
{3, 17, 4, 0, 13, 7, 760}, // [
{6, 13, 6, 0, 13, 10, 767}, // <backslash>
{3, 17, 4, 0, 13, 7, 777}, // ]
{7, 7, 8, 0, 13, 7, 784}, // ^
{11, 2, 12, 0, 2, 3, 791}, // _
{4, 5, 5, 0, 13, 3, 794}, // `
{8, 11, 9, 0, 11, 11, 797}, // a
{8, 13, 9, 0, 13, 13, 808}, // b
{7, 11, 8, 0, 11, 10, 821}, // c
{8, 13, 9, 0, 13, 13, 831}, // d
{8, 11, 9, 0, 11, 11, 844}, // e
{6, 13, 6, 0, 13, 10, 855}, // f
{8, 15, 9, 0, 11, 15, 865}, // g
{8, 13, 9, 0, 13, 13, 880}, // h
{2, 13, 3, 0, 13, 4, 893}, // i
{4, 17, 5, 0, 13, 9, 897}, // j
{8, 13, 9, 0, 13, 13, 906}, // k
{2, 13, 3, 0, 13, 4, 919}, // l
{12, 11, 13, 0, 11, 17, 923}, // m
{8, 11, 9, 0, 11, 11, 940}, // n
{8, 11, 9, 0, 11, 11, 951}, // o
{8, 15, 9, 0, 11, 15, 962}, // p
{8, 15, 9, 0, 11, 15, 977}, // q
{6, 11, 6, 0, 11, 9, 992}, // r
{7, 11, 8, 0, 11, 10, 1001}, // s
{6, 13, 6, 0, 13, 10, 1011}, // t
{8, 11, 9, 0, 11, 11, 1021}, // u
{9, 11, 10, 0, 11, 13, 1032}, // v
{12, 11, 13, 0, 11, 17, 1045}, // w
{8, 11, 9, 0, 11, 11, 1062}, // x
{9, 15, 10, 0, 11, 17, 1073}, // y
{8, 11, 9, 0, 11, 11, 1090}, // z
{4, 17, 5, 0, 13, 9, 1101}, // {
{2, 13, 3, 0, 13, 4, 1110}, // |
{4, 17, 5, 0, 13, 9, 1114}, // }
{11, 4, 12, 0, 9, 6, 1123}, // ~
{3, 6, 4, 0, 13, 3, 1129}, //
{3, 6, 4, 0, 13, 3, 1132}, //
{6, 6, 6, 0, 13, 5, 1135}, // “
{6, 6, 6, 0, 13, 5, 1140}, // ”
};
static const EpdUnicodeInterval pixelarial14Intervals[] = {
{0x20, 0x7E, 0x0},
{0x2018, 0x2019, 0x5F},
{0x201C, 0x201D, 0x61},
};
static const EpdFontData pixelarial14 = {
pixelarial14Bitmaps, pixelarial14Glyphs, pixelarial14Intervals, 3, 17, 13, -4, false,
};

View File

@@ -6,6 +6,8 @@
#include <map> #include <map>
#include "Epub/FsHelpers.h"
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) { bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
// open up the meta data to find where the content.opf file lives // open up the meta data to find where the content.opf file lives
size_t s; size_t s;
@@ -249,7 +251,20 @@ bool Epub::load() {
return true; return true;
} }
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); } bool Epub::clearCache() const {
if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
return true;
}
if (!FsHelpers::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 { void Epub::setupCacheDir() const {
if (SD.exists(cachePath.c_str())) { if (SD.exists(cachePath.c_str())) {

View File

@@ -50,7 +50,7 @@ class Epub {
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
bool load(); bool load();
void clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
const std::string& getCachePath() const; const std::string& getCachePath() const;
const std::string& getPath() const; const std::string& getPath() const;

View File

@@ -38,7 +38,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
} }
// start a new text block if needed // start a new text block if needed
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) { void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
if (currentTextBlock) { if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it // already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) { if (currentTextBlock->isEmpty()) {
@@ -46,11 +46,9 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
return; return;
} }
currentTextBlock->finish();
makePages(); makePages();
delete currentTextBlock;
} }
currentTextBlock = new TextBlock(style); currentTextBlock.reset(new ParsedText(style));
} }
void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
@@ -94,13 +92,13 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
} }
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(CENTER_ALIGN); self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->boldUntilDepth = min(self->boldUntilDepth, self->depth); self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) { if (strcmp(name, "br") == 0) {
self->startNewTextBlock(self->currentTextBlock->getStyle()); self->startNewTextBlock(self->currentTextBlock->getStyle());
} else { } else {
self->startNewTextBlock(JUSTIFIED); self->startNewTextBlock(TextBlock::JUSTIFIED);
} }
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = min(self->boldUntilDepth, self->depth); self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
@@ -119,13 +117,21 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
return; return;
} }
EpdFontStyle fontStyle = REGULAR;
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
fontStyle = BOLD_ITALIC;
} else if (self->boldUntilDepth < self->depth) {
fontStyle = BOLD;
} else if (self->italicUntilDepth < self->depth) {
fontStyle = ITALIC;
}
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
if (isWhitespace(s[i])) { if (isWhitespace(s[i])) {
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it // Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
self->partWordBuffer[self->partWordBufferIndex] = '\0'; self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; self->partWordBufferIndex = 0;
} }
// Skip the whitespace char // Skip the whitespace char
@@ -135,8 +141,7 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
// If we're about to run out of space, then cut the word off and start a new one // If we're about to run out of space, then cut the word off and start a new one
if (self->partWordBufferIndex >= MAX_WORD_SIZE) { if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
self->partWordBuffer[self->partWordBufferIndex] = '\0'; self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; self->partWordBufferIndex = 0;
} }
@@ -158,9 +163,17 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
if (shouldBreakText) { if (shouldBreakText) {
EpdFontStyle fontStyle = REGULAR;
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
fontStyle = BOLD_ITALIC;
} else if (self->boldUntilDepth < self->depth) {
fontStyle = BOLD;
} else if (self->italicUntilDepth < self->depth) {
fontStyle = ITALIC;
}
self->partWordBuffer[self->partWordBufferIndex] = '\0'; self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth, self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->italicUntilDepth < self->depth);
self->partWordBufferIndex = 0; self->partWordBufferIndex = 0;
} }
} }
@@ -184,7 +197,7 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
} }
bool EpubHtmlParserSlim::parseAndBuildPages() { bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED); startNewTextBlock(TextBlock::JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr); const XML_Parser parser = XML_ParserCreate(nullptr);
int done; int done;
@@ -240,10 +253,9 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
// Process last page if there is still text // Process last page if there is still text
if (currentTextBlock) { if (currentTextBlock) {
makePages(); makePages();
completePageFn(currentPage); completePageFn(std::move(currentPage));
currentPage = nullptr; currentPage.reset();
delete currentTextBlock; currentTextBlock.reset();
currentTextBlock = nullptr;
} }
return true; return true;
@@ -256,7 +268,7 @@ void EpubHtmlParserSlim::makePages() {
} }
if (!currentPage) { if (!currentPage) {
currentPage = new Page(); currentPage.reset(new Page());
currentPageNextY = marginTop; currentPageNextY = marginTop;
} }
@@ -266,30 +278,18 @@ void EpubHtmlParserSlim::makePages() {
// Long running task, make sure to let other things happen // Long running task, make sure to let other things happen
vTaskDelay(1); vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) { const auto lines = currentTextBlock->layoutAndExtractLines(renderer, fontId, marginLeft + marginRight);
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
for (const auto line : lines) { for (auto&& line : lines) {
if (currentPageNextY + lineHeight > pageHeight) { if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(currentPage); completePageFn(std::move(currentPage));
currentPage = new Page(); currentPage.reset(new Page());
currentPageNextY = marginTop; currentPageNextY = marginTop;
} }
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY)); currentPage->elements.push_back(std::make_shared<PageLine>(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight; currentPageNextY += lineHeight;
} }
// add some extra line between blocks // add some extra line between blocks
currentPageNextY += lineHeight / 2; currentPageNextY += 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

@@ -4,7 +4,9 @@
#include <climits> #include <climits>
#include <functional> #include <functional>
#include <memory>
#include "ParsedText.h"
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
class Page; class Page;
@@ -15,7 +17,7 @@ class GfxRenderer;
class EpubHtmlParserSlim { class EpubHtmlParserSlim {
const char* filepath; const char* filepath;
GfxRenderer& renderer; GfxRenderer& renderer;
std::function<void(Page*)> completePageFn; std::function<void(std::unique_ptr<Page>)> completePageFn;
int depth = 0; int depth = 0;
int skipUntilDepth = INT_MAX; int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
@@ -24,9 +26,9 @@ class EpubHtmlParserSlim {
// leave one char at end for null pointer // leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {}; char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0; int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr; std::unique_ptr<ParsedText> currentTextBlock = nullptr;
Page* currentPage = nullptr; std::unique_ptr<Page> currentPage = nullptr;
int currentPageNextY = 0; int16_t currentPageNextY = 0;
int fontId; int fontId;
float lineCompression; float lineCompression;
int marginTop; int marginTop;
@@ -34,7 +36,7 @@ class EpubHtmlParserSlim {
int marginBottom; int marginBottom;
int marginLeft; int marginLeft;
void startNewTextBlock(BLOCK_STYLE style); void startNewTextBlock(TextBlock::BLOCK_STYLE style);
void makePages(); void makePages();
// XML callbacks // XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
@@ -45,7 +47,7 @@ class EpubHtmlParserSlim {
explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId, explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
const float lineCompression, const int marginTop, const int marginRight, const float lineCompression, const int marginTop, const int marginRight,
const int marginBottom, const int marginLeft, const int marginBottom, const int marginLeft,
const std::function<void(Page*)>& completePageFn) const std::function<void(std::unique_ptr<Page>)>& completePageFn)
: filepath(filepath), : filepath(filepath),
renderer(renderer), renderer(renderer),
fontId(fontId), fontId(fontId),

View File

@@ -0,0 +1,36 @@
#include "FsHelpers.h"
#include <SD.h>
bool FsHelpers::removeDir(const char* path) {
// 1. Open the directory
File dir = SD.open(path);
if (!dir) {
return false;
}
if (!dir.isDirectory()) {
return false;
}
File file = dir.openNextFile();
while (file) {
String filePath = path;
if (!filePath.endsWith("/")) {
filePath += "/";
}
filePath += file.name();
if (file.isDirectory()) {
if (!removeDir(filePath.c_str())) {
return false;
}
} else {
if (!SD.remove(filePath.c_str())) {
return false;
}
}
file = dir.openNextFile();
}
return SD.rmdir(path);
}

View File

@@ -0,0 +1,6 @@
#pragma once
class FsHelpers {
public:
static bool removeDir(const char* path);
};

View File

@@ -3,7 +3,7 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
constexpr uint8_t PAGE_FILE_VERSION = 1; constexpr uint8_t PAGE_FILE_VERSION = 3;
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
@@ -15,18 +15,18 @@ void PageLine::serialize(std::ostream& os) {
block->serialize(os); block->serialize(os);
} }
PageLine* PageLine::deserialize(std::istream& is) { std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
int32_t xPos; int16_t xPos;
int32_t yPos; int16_t yPos;
serialization::readPod(is, xPos); serialization::readPod(is, xPos);
serialization::readPod(is, yPos); serialization::readPod(is, yPos);
const auto tb = TextBlock::deserialize(is); auto tb = TextBlock::deserialize(is);
return new PageLine(tb, xPos, yPos); return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
} }
void Page::render(GfxRenderer& renderer, const int fontId) const { void Page::render(GfxRenderer& renderer, const int fontId) const {
for (const auto element : elements) { for (auto& element : elements) {
element->render(renderer, fontId); element->render(renderer, fontId);
} }
} }
@@ -37,14 +37,14 @@ void Page::serialize(std::ostream& os) const {
const uint32_t count = elements.size(); const uint32_t count = elements.size();
serialization::writePod(os, count); serialization::writePod(os, count);
for (auto* el : elements) { for (const auto& el : elements) {
// Only PageLine exists currently // Only PageLine exists currently
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine)); serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
static_cast<PageLine*>(el)->serialize(os); el->serialize(os);
} }
} }
Page* Page::deserialize(std::istream& is) { std::unique_ptr<Page> Page::deserialize(std::istream& is) {
uint8_t version; uint8_t version;
serialization::readPod(is, version); serialization::readPod(is, version);
if (version != PAGE_FILE_VERSION) { if (version != PAGE_FILE_VERSION) {
@@ -52,7 +52,7 @@ Page* Page::deserialize(std::istream& is) {
return nullptr; return nullptr;
} }
auto* page = new Page(); auto page = std::unique_ptr<Page>(new Page());
uint32_t count; uint32_t count;
serialization::readPod(is, count); serialization::readPod(is, count);
@@ -62,10 +62,11 @@ Page* Page::deserialize(std::istream& is) {
serialization::readPod(is, tag); serialization::readPod(is, tag);
if (tag == TAG_PageLine) { if (tag == TAG_PageLine) {
auto* pl = PageLine::deserialize(is); auto pl = PageLine::deserialize(is);
page->elements.push_back(pl); page->elements.push_back(std::move(pl));
} else { } 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,7 @@
#pragma once #pragma once
#include <utility>
#include <vector>
#include "blocks/TextBlock.h" #include "blocks/TextBlock.h"
enum PageElementTag : uint8_t { enum PageElementTag : uint8_t {
@@ -8,9 +11,9 @@ enum PageElementTag : uint8_t {
// represents something that has been added to a page // represents something that has been added to a page
class PageElement { class PageElement {
public: public:
int xPos; int16_t xPos;
int yPos; int16_t yPos;
explicit PageElement(const int xPos, const int yPos) : xPos(xPos), yPos(yPos) {} explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default; virtual ~PageElement() = default;
virtual void render(GfxRenderer& renderer, int fontId) = 0; virtual void render(GfxRenderer& renderer, int fontId) = 0;
virtual void serialize(std::ostream& os) = 0; virtual void serialize(std::ostream& os) = 0;
@@ -18,27 +21,21 @@ class PageElement {
// a line from a block element // a line from a block element
class PageLine final : public PageElement { class PageLine final : public PageElement {
const TextBlock* block; std::shared_ptr<TextBlock> block;
public: public:
PageLine(const TextBlock* block, const int xPos, const int yPos) : PageElement(xPos, yPos), block(block) {} PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
~PageLine() override { delete block; } : PageElement(xPos, yPos), block(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId) override; void render(GfxRenderer& renderer, int fontId) override;
void serialize(std::ostream& os) override; void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is); static std::unique_ptr<PageLine> deserialize(std::istream& is);
}; };
class Page { class Page {
public: public:
~Page() {
for (const auto element : elements) {
delete element;
}
}
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
std::vector<PageElement*> elements; std::vector<std::shared_ptr<PageElement>> elements;
void render(GfxRenderer& renderer, int fontId) const; void render(GfxRenderer& renderer, int fontId) const;
void serialize(std::ostream& os) const; void serialize(std::ostream& os) const;
static Page* deserialize(std::istream& is); static std::unique_ptr<Page> deserialize(std::istream& is);
}; };

View File

@@ -0,0 +1,167 @@
#include "ParsedText.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
constexpr int MAX_COST = std::numeric_limits<int>::max();
void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
if (word.empty()) return;
words.push_back(std::move(word));
wordStyles.push_back(fontStyle);
}
// Consumes data to minimize memory usage
std::list<std::shared_ptr<TextBlock>> ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId,
const int horizontalMargin) {
if (words.empty()) {
return {};
}
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId);
std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount);
auto wordsIt = words.begin();
auto wordStylesIt = wordStyles.begin();
while (wordsIt != words.end()) {
wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt));
std::advance(wordsIt, 1);
std::advance(wordStylesIt, 1);
}
// DP table to store the minimum badness (cost) of lines starting at index i
std::vector<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
std::vector<size_t> ans(totalWordCount);
// Base Case
dp[totalWordCount - 1] = 0;
ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth;
dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) {
// Current line length: previous width + space + current word width
currlen += wordWidths[j] + spaceWidth;
if (currlen > pageWidth) {
break;
}
int cost;
if (j == totalWordCount - 1) {
cost = 0; // Last line
} else {
const int remainingSpace = pageWidth - currlen;
// Use long long for the square to prevent overflow
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
if (cost_ll > MAX_COST) {
cost = MAX_COST;
} else {
cost = static_cast<int>(cost_ll);
}
}
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
}
}
// Stores the index of the word that starts the next line (last_word_index + 1)
std::vector<size_t> lineBreakIndices;
size_t currentWordIndex = 0;
constexpr size_t MAX_LINES = 1000;
while (currentWordIndex < totalWordCount) {
if (lineBreakIndices.size() >= MAX_LINES) {
break;
}
size_t nextBreakIndex = ans[currentWordIndex] + 1;
lineBreakIndices.push_back(nextBreakIndex);
currentWordIndex = nextBreakIndex;
}
std::list<std::shared_ptr<TextBlock>> lines;
// Initialize iterators for consumption
auto wordStartIt = words.begin();
auto wordStyleStartIt = wordStyles.begin();
size_t wordWidthIndex = 0;
size_t lastBreakAt = 0;
for (const size_t lineBreak : lineBreakIndices) {
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate end iterators for the range to splice
auto wordEndIt = wordStartIt;
auto wordStyleEndIt = wordStyleStartIt;
std::advance(wordEndIt, lineWordCount);
std::advance(wordStyleEndIt, lineWordCount);
// Calculate total word width for this line
int lineWordWidthSum = 0;
for (size_t i = 0; i < lineWordCount; ++i) {
lineWordWidthSum += wordWidths[wordWidthIndex + i];
}
// Calculate spacing
const int spareSpace = pageWidth - lineWordWidthSum;
int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount;
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
}
// Calculate initial x position
uint16_t xpos = 0;
if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == TextBlock::CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
}
// Pre-calculate X positions for words
std::list<uint16_t> lineXPos;
for (size_t i = 0; i < lineWordCount; ++i) {
const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i];
lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing;
}
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
std::list<std::string> lineWords;
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
std::list<EpdFontStyle> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
lines.push_back(
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
// Update pointers/indices for the next line
wordStartIt = wordEndIt;
wordStyleStartIt = wordStyleEndIt;
wordWidthIndex += lineWordCount;
lastBreakAt = lineBreak;
}
return lines;
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <EpdFontFamily.h>
#include <cstdint>
#include <list>
#include <memory>
#include <string>
#include "blocks/TextBlock.h"
class GfxRenderer;
class ParsedText {
std::list<std::string> words;
std::list<EpdFontStyle> wordStyles;
TextBlock::BLOCK_STYLE style;
public:
explicit ParsedText(const TextBlock::BLOCK_STYLE style) : style(style) {}
~ParsedText() = default;
void addWord(std::string word, EpdFontStyle fontStyle);
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
TextBlock::BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() const { return words.empty(); }
std::list<std::shared_ptr<TextBlock>> layoutAndExtractLines(const GfxRenderer& renderer, int fontId,
int horizontalMargin);
};

View File

@@ -1,17 +1,17 @@
#include "Section.h" #include "Section.h"
#include <GfxRenderer.h>
#include <SD.h> #include <SD.h>
#include <Serialization.h>
#include <fstream> #include <fstream>
#include "EpubHtmlParserSlim.h" #include "EpubHtmlParserSlim.h"
#include "FsHelpers.h"
#include "Page.h" #include "Page.h"
#include "Serialization.h"
constexpr uint8_t SECTION_FILE_VERSION = 3; constexpr uint8_t SECTION_FILE_VERSION = 4;
void Section::onPageComplete(const Page* page) { void Section::onPageComplete(std::unique_ptr<Page> page) {
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
std::ofstream outputFile("/sd" + filePath); std::ofstream outputFile("/sd" + filePath);
@@ -21,7 +21,6 @@ void Section::onPageComplete(const Page* page) {
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount); Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++; pageCount++;
delete page;
} }
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
@@ -57,8 +56,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
serialization::readPod(inputFile, version); serialization::readPod(inputFile, version);
if (version != SECTION_FILE_VERSION) { if (version != SECTION_FILE_VERSION) {
inputFile.close(); inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version); Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
clearCache();
return false; return false;
} }
@@ -74,8 +73,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop || if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) { marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
inputFile.close(); inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache();
return false; return false;
} }
} }
@@ -91,7 +90,21 @@ void Section::setupCacheDir() const {
SD.mkdir(cachePath.c_str()); SD.mkdir(cachePath.c_str());
} }
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); } // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const {
if (!SD.exists(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
return true;
}
if (!FsHelpers::removeDir(cachePath.c_str())) {
Serial.printf("[%lu] [SCT] Failed to clear cache\n", millis());
return false;
}
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
return true;
}
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
const int marginRight, const int marginBottom, const int marginLeft) { const int marginRight, const int marginBottom, const int marginLeft) {
@@ -114,8 +127,9 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath; const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight, EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); }); marginBottom, marginLeft,
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str()); SD.remove(tmpHtmlPath.c_str());
@@ -129,7 +143,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
return true; return true;
} }
Page* Section::loadPageFromSD() const { std::unique_ptr<Page> Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin"; const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) { if (!SD.exists(filePath.c_str() + 3)) {
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str()); Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
@@ -137,7 +151,7 @@ Page* Section::loadPageFromSD() const {
} }
std::ifstream inputFile(filePath); std::ifstream inputFile(filePath);
Page* p = Page::deserialize(inputFile); auto page = Page::deserialize(inputFile);
inputFile.close(); inputFile.close();
return p; return page;
} }

View File

@@ -1,24 +1,26 @@
#pragma once #pragma once
#include <memory>
#include "Epub.h" #include "Epub.h"
class Page; class Page;
class GfxRenderer; class GfxRenderer;
class Section { class Section {
Epub* epub; std::shared_ptr<Epub> epub;
const int spineIndex; const int spineIndex;
GfxRenderer& renderer; GfxRenderer& renderer;
std::string cachePath; std::string cachePath;
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft) const; int marginLeft) const;
void onPageComplete(const Page* page); void onPageComplete(std::unique_ptr<Page> page);
public: public:
int pageCount = 0; int pageCount = 0;
int currentPage = 0; int currentPage = 0;
explicit Section(Epub* epub, const int spineIndex, GfxRenderer& renderer) explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub), spineIndex(spineIndex), renderer(renderer) { : epub(epub), spineIndex(spineIndex), renderer(renderer) {
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex); cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
} }
@@ -26,8 +28,8 @@ class Section {
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft); int marginLeft);
void setupCacheDir() const; void setupCacheDir() const;
void clearCache() const; bool clearCache() const;
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
int marginLeft); int marginLeft);
Page* loadPageFromSD() const; std::unique_ptr<Page> loadPageFromSD() const;
}; };

View File

@@ -3,170 +3,17 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <Serialization.h> #include <Serialization.h>
void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) {
if (word.length() == 0) return;
words.push_back(word);
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
}
std::list<TextBlock*> TextBlock::splitIntoLines(const GfxRenderer& renderer, const int fontId,
const int horizontalMargin) {
const int totalWordCount = words.size();
const int pageWidth = GfxRenderer::getScreenWidth() - horizontalMargin;
const int spaceWidth = renderer.getSpaceWidth(fontId);
words.shrink_to_fit();
wordStyles.shrink_to_fit();
wordXpos.reserve(totalWordCount);
// measure each word
uint16_t wordWidths[totalWordCount];
for (int i = 0; i < totalWordCount; i++) {
// measure the word
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN) {
if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else {
fontStyle = BOLD;
}
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
const int width = renderer.getTextWidth(fontId, words[i].c_str(), fontStyle);
wordWidths[i] = width;
}
// now apply the dynamic programming algorithm to find the best line breaks
// DP table in which dp[i] represents cost of line starting with word words[i]
int dp[totalWordCount];
// Array in which ans[i] store index of last word in line starting with word
// word[i]
size_t ans[totalWordCount];
// If only one word is present then only one line is required. Cost of last
// line is zero. Hence cost of this line is zero. Ending point is also n-1 as
// single word is present
dp[totalWordCount - 1] = 0;
ans[totalWordCount - 1] = totalWordCount - 1;
// Make each word first word of line by iterating over each index in arr.
for (int i = totalWordCount - 2; i >= 0; i--) {
int currlen = -1;
dp[i] = INT_MAX;
// Variable to store possible minimum cost of line.
int cost;
// Keep on adding words in current line by iterating from starting word upto
// last word in arr.
for (int j = i; j < totalWordCount; j++) {
// Update the width of the words in current line + the space between two
// words.
currlen += wordWidths[j] + spaceWidth;
// If we're bigger than the current pagewidth then we can't add more words
if (currlen > pageWidth) break;
// if we've run out of words then this is last line and the cost should be
// 0 Otherwise the cost is the sqaure of the left over space + the costs
// of all the previous lines
if (j == totalWordCount - 1)
cost = 0;
else
cost = (pageWidth - currlen) * (pageWidth - currlen) + dp[j + 1];
// Check if this arrangement gives minimum cost for line starting with
// word words[i].
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j;
}
}
}
// We can now iterate through the answer to find the line break positions
std::list<uint16_t> lineBreaks;
for (size_t i = 0; i < totalWordCount;) {
i = ans[i] + 1;
if (i > totalWordCount) {
break;
}
lineBreaks.push_back(i);
// Text too big, just exit
if (lineBreaks.size() > 1000) {
break;
}
}
std::list<TextBlock*> lines;
// With the line breaks calculated we can now position the words along the
// line
int startWord = 0;
for (const auto lineBreak : lineBreaks) {
const int lineWordCount = lineBreak - startWord;
int lineWordWidthSum = 0;
for (int i = startWord; i < lineBreak; i++) {
lineWordWidthSum += wordWidths[i];
}
// Calculate spacing between words
const uint16_t spareSpace = pageWidth - lineWordWidthSum;
uint16_t spacing = spaceWidth;
// evenly space words if using justified style, not the last line, and at
// least 2 words
if (style == JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
}
uint16_t xpos = 0;
if (style == RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
} else if (style == CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
}
for (int i = startWord; i < lineBreak; i++) {
wordXpos[i] = xpos;
xpos += wordWidths[i] + spacing;
}
std::vector<std::string> lineWords;
std::vector<uint16_t> lineXPos;
std::vector<uint8_t> lineWordStyles;
lineWords.reserve(lineWordCount);
lineXPos.reserve(lineWordCount);
lineWordStyles.reserve(lineWordCount);
for (int i = startWord; i < lineBreak; i++) {
lineWords.push_back(words[i]);
lineXPos.push_back(wordXpos[i]);
lineWordStyles.push_back(wordStyles[i]);
}
const auto textLine = new TextBlock(lineWords, lineXPos, lineWordStyles, style);
lines.push_back(textLine);
startWord = lineBreak;
}
return lines;
}
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
auto wordIt = words.begin();
auto wordStylesIt = wordStyles.begin();
auto wordXposIt = wordXpos.begin();
for (int i = 0; i < words.size(); i++) { for (int i = 0; i < words.size(); i++) {
// render the word renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN && wordStyles[i] & ITALIC_SPAN) { std::advance(wordIt, 1);
fontStyle = BOLD_ITALIC; std::advance(wordStylesIt, 1);
} else if (wordStyles[i] & BOLD_SPAN) { std::advance(wordXposIt, 1);
fontStyle = BOLD;
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
} }
} }
@@ -190,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const {
serialization::writePod(os, style); serialization::writePod(os, style);
} }
TextBlock* TextBlock::deserialize(std::istream& is) { std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
uint32_t wc, xc, sc; uint32_t wc, xc, sc;
std::vector<std::string> words; std::list<std::string> words;
std::vector<uint16_t> wordXpos; std::list<uint16_t> wordXpos;
std::vector<uint8_t> wordStyles; std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style; BLOCK_STYLE style;
// words // words
@@ -215,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& is) {
// style // style
serialization::readPod(is, style); serialization::readPod(is, style);
return new TextBlock(words, wordXpos, wordStyles, style); return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
} }

View File

@@ -1,15 +1,15 @@
#pragma once #pragma once
#include <EpdFontFamily.h>
#include <list> #include <list>
#include <memory>
#include <string> #include <string>
#include <vector>
#include "Block.h" #include "Block.h"
enum SPAN_STYLE : uint8_t { // represents a block of words in the html document
BOLD_SPAN = 1, class TextBlock final : public Block {
ITALIC_SPAN = 2, public:
};
enum BLOCK_STYLE : uint8_t { enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0, JUSTIFIED = 0,
LEFT_ALIGN = 1, LEFT_ALIGN = 1,
@@ -17,34 +17,24 @@ enum BLOCK_STYLE : uint8_t {
RIGHT_ALIGN = 3, RIGHT_ALIGN = 3,
}; };
// represents a block of words in the html document private:
class TextBlock final : public Block { std::list<std::string> words;
// pointer to each word std::list<uint16_t> wordXpos;
std::vector<std::string> words; std::list<EpdFontStyle> wordStyles;
// x position of each word
std::vector<uint16_t> wordXpos;
// the styles of each word
std::vector<uint8_t> wordStyles;
// the style of the block - left, center, right aligned
BLOCK_STYLE style; BLOCK_STYLE style;
public: public:
explicit TextBlock(const BLOCK_STYLE style) : style(style) {} explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos, const BLOCK_STYLE style)
// the styles of each word : words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
~TextBlock() override = default; ~TextBlock() override = default;
void addWord(const std::string& word, bool is_bold, bool is_italic);
void setStyle(const BLOCK_STYLE style) { this->style = style; } void setStyle(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE getStyle() const { return style; } BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {}; void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // given a renderer works out where to break the words into lines
std::list<TextBlock*> splitIntoLines(const GfxRenderer& renderer, int fontId, int horizontalMargin);
void render(const GfxRenderer& renderer, int fontId, int x, int y) const; void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
BlockType getType() override { return TEXT_BLOCK; } BlockType getType() override { return TEXT_BLOCK; }
void serialize(std::ostream& os) const; void serialize(std::ostream& os) const;
static TextBlock* deserialize(std::istream& is); static std::unique_ptr<TextBlock> deserialize(std::istream& is);
}; };

View File

@@ -162,8 +162,14 @@ int GfxRenderer::getLineHeight(const int fontId) const {
return fontMap.at(fontId).getData(REGULAR)->advanceY; return fontMap.at(fontId).getData(REGULAR)->advanceY;
} }
uint8_t *GfxRenderer::getFrameBuffer() const {
return einkDisplay.getFrameBuffer();
}
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); } void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }

View File

@@ -1,11 +1,10 @@
#pragma once #pragma once
#include <EInkDisplay.h> #include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <map> #include <map>
#include "EpdFontFamily.h"
class GfxRenderer { class GfxRenderer {
public: public:
enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
@@ -47,7 +46,9 @@ class GfxRenderer {
int getLineHeight(int fontId) const; int getLineHeight(int fontId) const;
// Low level functions // Low level functions
uint8_t* getFrameBuffer() const;
void swapBuffers() const; void swapBuffers() const;
void grayscaleRevert() const;
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;

View File

@@ -1,5 +1,5 @@
[platformio] [platformio]
crosspoint_version = 0.3.0 crosspoint_version = 0.4.0
default_envs = default default_envs = default
[base] [base]
@@ -20,6 +20,7 @@ build_flags =
# https://libexpat.github.io/doc/api/latest/#XML_GE # https://libexpat.github.io/doc/api/latest/#XML_GE
-DXML_GE=0 -DXML_GE=0
-DXML_CONTEXT_BYTES=1024 -DXML_CONTEXT_BYTES=1024
-std=c++2a
; Board configuration ; Board configuration
board_build.flash_mode = dio board_build.flash_mode = dio

View File

@@ -23,7 +23,7 @@
/** /**
* Generated with: * Generated with:
* ruby -rdigest -e 'puts [ * ruby -rdigest -e 'puts [
* "./lib/EpdFont/builtinFonts/babyblue.h", * "./lib/EpdFont/builtinFonts/pixelarial14.h",
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)' * ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
*/ */
#define SMALL_FONT_ID 141891058 #define SMALL_FONT_ID 2037928017

View File

@@ -5,16 +5,16 @@
#include <InputManager.h> #include <InputManager.h>
#include <SD.h> #include <SD.h>
#include <SPI.h> #include <SPI.h>
#include <builtinFonts/bookerly_2b.h>
#include <builtinFonts/bookerly_bold_2b.h>
#include <builtinFonts/bookerly_bold_italic_2b.h>
#include <builtinFonts/bookerly_italic_2b.h>
#include <builtinFonts/pixelarial14.h>
#include <builtinFonts/ubuntu_10.h>
#include <builtinFonts/ubuntu_bold_10.h>
#include "Battery.h" #include "Battery.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "builtinFonts/babyblue.h"
#include "builtinFonts/bookerly_2b.h"
#include "builtinFonts/bookerly_bold_2b.h"
#include "builtinFonts/bookerly_bold_italic_2b.h"
#include "builtinFonts/bookerly_italic_2b.h"
#include "builtinFonts/ubuntu_10.h"
#include "builtinFonts/ubuntu_bold_10.h"
#include "config.h" #include "config.h"
#include "screens/BootLogoScreen.h" #include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h" #include "screens/EpubReaderScreen.h"
@@ -49,7 +49,7 @@ EpdFont bookerlyItalicFont(&bookerly_italic_2b);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b); EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont); EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue); EpdFont smallFont(&pixelarial14);
EpdFontFamily smallFontFamily(&smallFont); EpdFontFamily smallFontFamily(&smallFont);
EpdFont ubuntu10Font(&ubuntu_10); EpdFont ubuntu10Font(&ubuntu_10);
@@ -62,19 +62,18 @@ constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
// Time required to enter sleep mode // Time required to enter sleep mode
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000; constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
Epub* loadEpub(const std::string& path) { std::unique_ptr<Epub> loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) { if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str()); Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr; return nullptr;
} }
const auto epub = new Epub(path, "/.crosspoint"); auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) { if (epub->load()) {
return epub; return epub;
} }
Serial.printf("[%lu] [ ] Failed to load epub\n", millis()); Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
delete epub;
return nullptr; return nullptr;
} }
@@ -151,12 +150,12 @@ void onSelectEpubFile(const std::string& path) {
exitScreen(); exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading...")); enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
Epub* epub = loadEpub(path); auto epub = loadEpub(path);
if (epub) { if (epub) {
appState.openEpubPath = path; appState.openEpubPath = path;
appState.saveToFile(); appState.saveToFile();
exitScreen(); exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else { } else {
exitScreen(); exitScreen();
enterNewScreen( enterNewScreen(
@@ -172,11 +171,7 @@ void onGoHome() {
} }
void setup() { void setup() {
// Begin serial only if USB connected
pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200); Serial.begin(115200);
}
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
@@ -206,10 +201,10 @@ void setup() {
appState.loadFromFile(); appState.loadFromFile();
if (!appState.openEpubPath.empty()) { if (!appState.openEpubPath.empty()) {
Epub* epub = loadEpub(appState.openEpubPath); auto epub = loadEpub(appState.openEpubPath);
if (epub) { if (epub) {
exitScreen(); exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome)); enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
return; return;

View File

@@ -10,9 +10,9 @@
constexpr int PAGES_PER_REFRESH = 15; constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700; constexpr unsigned long SKIP_CHAPTER_MS = 700;
constexpr float lineCompression = 0.95f; constexpr float lineCompression = 0.95f;
constexpr int marginTop = 11; constexpr int marginTop = 8;
constexpr int marginRight = 10; constexpr int marginRight = 10;
constexpr int marginBottom = 30; constexpr int marginBottom = 22;
constexpr int marginLeft = 10; constexpr int marginLeft = 10;
void EpubReaderScreen::taskTrampoline(void* param) { void EpubReaderScreen::taskTrampoline(void* param) {
@@ -60,10 +60,8 @@ void EpubReaderScreen::onExit() {
} }
vSemaphoreDelete(renderingMutex); vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr; renderingMutex = nullptr;
delete section; section.reset();
section = nullptr; epub.reset();
delete epub;
epub = nullptr;
} }
void EpubReaderScreen::handleInput() { void EpubReaderScreen::handleInput() {
@@ -88,8 +86,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0; nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1; currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
updateRequired = true; updateRequired = true;
return; return;
@@ -109,8 +106,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX; nextPageNumber = UINT16_MAX;
currentSpineIndex--; currentSpineIndex--;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
updateRequired = true; updateRequired = true;
@@ -122,8 +118,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0; nextPageNumber = 0;
currentSpineIndex++; currentSpineIndex++;
delete section; section.reset();
section = nullptr;
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
updateRequired = true; updateRequired = true;
@@ -155,7 +150,7 @@ void EpubReaderScreen::renderScreen() {
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex); const auto filepath = epub->getSpineItem(currentSpineIndex);
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
section = new Section(epub, currentSpineIndex, renderer); section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) { marginLeft)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
@@ -167,7 +162,10 @@ void EpubReaderScreen::renderScreen() {
constexpr int y = 50; constexpr int y = 50;
const int w = textWidth + margin * 2; const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2; const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
renderer.grayscaleRevert();
uint8_t *fb1 = renderer.getFrameBuffer();
renderer.swapBuffers(); renderer.swapBuffers();
memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE);
renderer.fillRect(x, y, w, h, 0); renderer.fillRect(x, y, w, h, 0);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing..."); renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
renderer.drawRect(x + 5, y + 5, w - 10, h - 10); renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
@@ -179,8 +177,7 @@ void EpubReaderScreen::renderScreen() {
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
marginLeft)) { marginLeft)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
delete section; section.reset();
section = nullptr;
return; return;
} }
} else { } else {
@@ -212,11 +209,18 @@ void EpubReaderScreen::renderScreen() {
return; return;
} }
const Page* p = section->loadPageFromSD(); {
auto p = section->loadPageFromSD();
if (!p) {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
}
const auto start = millis(); const auto start = millis();
renderContents(p); renderContents(std::move(p));
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
delete p; }
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE); File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4]; uint8_t data[4];
@@ -228,8 +232,8 @@ void EpubReaderScreen::renderScreen() {
f.close(); f.close();
} }
void EpubReaderScreen::renderContents(const Page* p) { void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderStatusBar(); renderStatusBar();
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH); renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@@ -244,13 +248,13 @@ void EpubReaderScreen::renderContents(const Page* p) {
{ {
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleLsbBuffers(); renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer // Render and copy to MSB buffer
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB); renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
p->render(renderer, READER_FONT_ID); page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleMsbBuffers(); renderer.copyGrayscaleMsbBuffers();
// display grayscale part // display grayscale part
@@ -260,17 +264,18 @@ void EpubReaderScreen::renderContents(const Page* p) {
} }
void EpubReaderScreen::renderStatusBar() const { void EpubReaderScreen::renderStatusBar() const {
constexpr auto textY = 776;
// Right aligned text for progress counter // Right aligned text for progress counter
const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount); const std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, 776, renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str()); progress.c_str());
// Left aligned battery icon and percentage // Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage(); const uint16_t percentage = battery.readPercentage();
const auto percentageText = std::to_string(percentage) + "%"; const auto percentageText = std::to_string(percentage) + "%";
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, 776, percentageText.c_str()); renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body // 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15; constexpr int batteryWidth = 15;
@@ -286,8 +291,8 @@ void EpubReaderScreen::renderStatusBar() const {
renderer.drawLine(x, y, x, y + batteryHeight - 1); renderer.drawLine(x, y, x, y + batteryHeight - 1);
// Battery end // Battery end
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1); renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3); renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
renderer.drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3); renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3); renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
// The +1 is to round up, so that we always fill at least one pixel // The +1 is to round up, so that we always fill at least one pixel
@@ -310,5 +315,5 @@ void EpubReaderScreen::renderStatusBar() const {
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, 777, title.c_str()); renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
} }

View File

@@ -8,8 +8,8 @@
#include "Screen.h" #include "Screen.h"
class EpubReaderScreen final : public Screen { class EpubReaderScreen final : public Screen {
Epub* epub; std::shared_ptr<Epub> epub;
Section* section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
@@ -21,13 +21,13 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void renderScreen(); void renderScreen();
void renderContents(const Page* p); void renderContents(std::unique_ptr<Page> p);
void renderStatusBar() const; void renderStatusBar() const;
public: public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, Epub* epub, explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoHome) const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {} : Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void handleInput() override; void handleInput() override;