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
- [ ] 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
@@ -58,24 +81,12 @@ 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

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 "Epub/FsHelpers.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;
@@ -249,7 +251,20 @@ bool Epub::load() {
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 {
if (SD.exists(cachePath.c_str())) {

View File

@@ -50,7 +50,7 @@ class Epub {
~Epub() = default;
std::string& getBasePath() { return contentBasePath; }
bool load();
void clearCache() const;
bool clearCache() const;
void setupCacheDir() const;
const std::string& getCachePath() 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
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
void EpubHtmlParserSlim::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) {
@@ -46,11 +46,9 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
return;
}
currentTextBlock->finish();
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) {
@@ -94,13 +92,13 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
}
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
self->startNewTextBlock(CENTER_ALIGN);
self->startNewTextBlock(TextBlock::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);
self->startNewTextBlock(TextBlock::JUSTIFIED);
}
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
@@ -119,13 +117,21 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
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++) {
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->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->partWordBufferIndex = 0;
}
// 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 (self->partWordBufferIndex >= MAX_WORD_SIZE) {
self->partWordBuffer[self->partWordBufferIndex] = '\0';
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
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;
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->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
self->italicUntilDepth < self->depth);
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
self->partWordBufferIndex = 0;
}
}
@@ -184,7 +197,7 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
}
bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED);
startNewTextBlock(TextBlock::JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr);
int done;
@@ -240,10 +253,9 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
// Process last page if there is still text
if (currentTextBlock) {
makePages();
completePageFn(currentPage);
currentPage = nullptr;
delete currentTextBlock;
currentTextBlock = nullptr;
completePageFn(std::move(currentPage));
currentPage.reset();
currentTextBlock.reset();
}
return true;
@@ -256,7 +268,7 @@ void EpubHtmlParserSlim::makePages() {
}
if (!currentPage) {
currentPage = new Page();
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
@@ -266,30 +278,18 @@ void EpubHtmlParserSlim::makePages() {
// Long running task, make sure to let other things happen
vTaskDelay(1);
if (currentTextBlock->getType() == TEXT_BLOCK) {
const auto lines = currentTextBlock->splitIntoLines(renderer, fontId, marginLeft + marginRight);
const auto lines = currentTextBlock->layoutAndExtractLines(renderer, fontId, marginLeft + marginRight);
for (const auto line : lines) {
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(currentPage);
currentPage = new Page();
currentPageNextY = marginTop;
}
currentPage->elements.push_back(new PageLine(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
for (auto&& line : lines) {
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(std::move(currentPage));
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
// add some extra line between blocks
currentPageNextY += lineHeight / 2;
currentPage->elements.push_back(std::make_shared<PageLine>(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
}
// 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;
// }
// add some extra line between blocks
currentPageNextY += lineHeight / 2;
}

View File

@@ -4,7 +4,9 @@
#include <climits>
#include <functional>
#include <memory>
#include "ParsedText.h"
#include "blocks/TextBlock.h"
class Page;
@@ -15,7 +17,7 @@ class GfxRenderer;
class EpubHtmlParserSlim {
const char* filepath;
GfxRenderer& renderer;
std::function<void(Page*)> completePageFn;
std::function<void(std::unique_ptr<Page>)> completePageFn;
int depth = 0;
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
@@ -24,9 +26,9 @@ class EpubHtmlParserSlim {
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
int partWordBufferIndex = 0;
TextBlock* currentTextBlock = nullptr;
Page* currentPage = nullptr;
int currentPageNextY = 0;
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
std::unique_ptr<Page> currentPage = nullptr;
int16_t currentPageNextY = 0;
int fontId;
float lineCompression;
int marginTop;
@@ -34,7 +36,7 @@ class EpubHtmlParserSlim {
int marginBottom;
int marginLeft;
void startNewTextBlock(BLOCK_STYLE style);
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
void makePages();
// XML callbacks
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,
const float lineCompression, const int marginTop, const int marginRight,
const int marginBottom, const int marginLeft,
const std::function<void(Page*)>& completePageFn)
const std::function<void(std::unique_ptr<Page>)>& completePageFn)
: filepath(filepath),
renderer(renderer),
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 <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); }
@@ -15,18 +15,18 @@ void PageLine::serialize(std::ostream& os) {
block->serialize(os);
}
PageLine* PageLine::deserialize(std::istream& is) {
int32_t xPos;
int32_t yPos;
std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
int16_t xPos;
int16_t yPos;
serialization::readPod(is, xPos);
serialization::readPod(is, yPos);
const auto tb = TextBlock::deserialize(is);
return new PageLine(tb, xPos, yPos);
auto tb = TextBlock::deserialize(is);
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
}
void Page::render(GfxRenderer& renderer, const int fontId) const {
for (const auto element : elements) {
for (auto& element : elements) {
element->render(renderer, fontId);
}
}
@@ -37,14 +37,14 @@ void Page::serialize(std::ostream& os) const {
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);
el->serialize(os);
}
}
Page* Page::deserialize(std::istream& is) {
std::unique_ptr<Page> Page::deserialize(std::istream& is) {
uint8_t version;
serialization::readPod(is, version);
if (version != PAGE_FILE_VERSION) {
@@ -52,7 +52,7 @@ Page* Page::deserialize(std::istream& is) {
return nullptr;
}
auto* page = new Page();
auto page = std::unique_ptr<Page>(new Page());
uint32_t count;
serialization::readPod(is, count);
@@ -62,10 +62,11 @@ Page* Page::deserialize(std::istream& is) {
serialization::readPod(is, tag);
if (tag == TAG_PageLine) {
auto* pl = PageLine::deserialize(is);
page->elements.push_back(pl);
auto pl = PageLine::deserialize(is);
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,7 @@
#pragma once
#include <utility>
#include <vector>
#include "blocks/TextBlock.h"
enum PageElementTag : uint8_t {
@@ -8,9 +11,9 @@ enum PageElementTag : uint8_t {
// represents something that has been added to a page
class PageElement {
public:
int xPos;
int yPos;
explicit PageElement(const int xPos, const int yPos) : xPos(xPos), 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(GfxRenderer& renderer, int fontId) = 0;
virtual void serialize(std::ostream& os) = 0;
@@ -18,27 +21,21 @@ class PageElement {
// 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 xPos, const int yPos) : PageElement(xPos, yPos), block(block) {}
~PageLine() override { delete block; }
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) override;
void serialize(std::ostream& os) override;
static PageLine* deserialize(std::istream& is);
static std::unique_ptr<PageLine> deserialize(std::istream& is);
};
class Page {
public:
~Page() {
for (const auto element : elements) {
delete element;
}
}
// 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 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 <GfxRenderer.h>
#include <SD.h>
#include <Serialization.h>
#include <fstream>
#include "EpubHtmlParserSlim.h"
#include "FsHelpers.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";
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);
pageCount++;
delete page;
}
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);
if (version != SECTION_FILE_VERSION) {
inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
clearCache();
return false;
}
@@ -74,8 +73,8 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
inputFile.close();
clearCache();
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
clearCache();
return false;
}
}
@@ -91,7 +90,21 @@ void Section::setupCacheDir() const {
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,
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;
auto visitor = EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft, [this](const Page* page) { this->onPageComplete(page); });
EpubHtmlParserSlim visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
marginBottom, marginLeft,
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
success = visitor.parseAndBuildPages();
SD.remove(tmpHtmlPath.c_str());
@@ -129,7 +143,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression,
return true;
}
Page* Section::loadPageFromSD() const {
std::unique_ptr<Page> Section::loadPageFromSD() const {
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
if (!SD.exists(filePath.c_str() + 3)) {
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);
Page* p = Page::deserialize(inputFile);
auto page = Page::deserialize(inputFile);
inputFile.close();
return p;
return page;
}

View File

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

View File

@@ -3,170 +3,17 @@
#include <GfxRenderer.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 {
auto wordIt = words.begin();
auto wordStylesIt = wordStyles.begin();
auto wordXposIt = wordXpos.begin();
for (int i = 0; i < words.size(); i++) {
// render the word
EpdFontStyle fontStyle = REGULAR;
if (wordStyles[i] & BOLD_SPAN && wordStyles[i] & ITALIC_SPAN) {
fontStyle = BOLD_ITALIC;
} else if (wordStyles[i] & BOLD_SPAN) {
fontStyle = BOLD;
} else if (wordStyles[i] & ITALIC_SPAN) {
fontStyle = ITALIC;
}
renderer.drawText(fontId, x + wordXpos[i], y, words[i].c_str(), true, fontStyle);
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
std::advance(wordIt, 1);
std::advance(wordStylesIt, 1);
std::advance(wordXposIt, 1);
}
}
@@ -190,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const {
serialization::writePod(os, style);
}
TextBlock* TextBlock::deserialize(std::istream& is) {
std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
uint32_t wc, xc, sc;
std::vector<std::string> words;
std::vector<uint16_t> wordXpos;
std::vector<uint8_t> wordStyles;
std::list<std::string> words;
std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style;
// words
@@ -215,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& 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,50 +1,40 @@
#pragma once
#include <EpdFontFamily.h>
#include <list>
#include <memory>
#include <string>
#include <vector>
#include "Block.h"
enum SPAN_STYLE : uint8_t {
BOLD_SPAN = 1,
ITALIC_SPAN = 2,
};
enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
// represents a block of words in the html document
class TextBlock final : public Block {
// pointer to each word
std::vector<std::string> words;
// x position of each word
std::vector<uint16_t> wordXpos;
// the styles of each word
std::vector<uint8_t> wordStyles;
public:
enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
};
// the style of the block - left, center, right aligned
private:
std::list<std::string> words;
std::list<uint16_t> wordXpos;
std::list<EpdFontStyle> wordStyles;
BLOCK_STYLE style;
public:
explicit TextBlock(const BLOCK_STYLE style) : style(style) {}
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos,
// the styles of each word
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
const BLOCK_STYLE style)
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
~TextBlock() override = default;
void addWord(const std::string& word, bool is_bold, bool is_italic);
void setStyle(const BLOCK_STYLE style) { this->style = style; }
BLOCK_STYLE getStyle() const { return style; }
bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {};
// 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;
BlockType getType() override { return TEXT_BLOCK; }
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;
}
uint8_t *GfxRenderer::getFrameBuffer() const {
return einkDisplay.getFrameBuffer();
}
void GfxRenderer::swapBuffers() const { einkDisplay.swapBuffers(); }
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }

View File

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

View File

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

View File

@@ -23,7 +23,7 @@
/**
* Generated with:
* 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)'
*/
#define SMALL_FONT_ID 141891058
#define SMALL_FONT_ID 2037928017

View File

@@ -5,16 +5,16 @@
#include <InputManager.h>
#include <SD.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 "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 "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h"
@@ -49,7 +49,7 @@ EpdFont bookerlyItalicFont(&bookerly_italic_2b);
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
EpdFont smallFont(&babyblue);
EpdFont smallFont(&pixelarial14);
EpdFontFamily smallFontFamily(&smallFont);
EpdFont ubuntu10Font(&ubuntu_10);
@@ -62,19 +62,18 @@ constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
// Time required to enter sleep mode
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())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
const auto epub = new Epub(path, "/.crosspoint");
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
delete epub;
return nullptr;
}
@@ -151,12 +150,12 @@ void onSelectEpubFile(const std::string& path) {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
Epub* epub = loadEpub(path);
auto epub = loadEpub(path);
if (epub) {
appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
} else {
exitScreen();
enterNewScreen(
@@ -172,11 +171,7 @@ void onGoHome() {
}
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());
@@ -206,10 +201,10 @@ void setup() {
appState.loadFromFile();
if (!appState.openEpubPath.empty()) {
Epub* epub = loadEpub(appState.openEpubPath);
auto epub = loadEpub(appState.openEpubPath);
if (epub) {
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
waitForPowerRelease();
return;

View File

@@ -10,9 +10,9 @@
constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700;
constexpr float lineCompression = 0.95f;
constexpr int marginTop = 11;
constexpr int marginTop = 8;
constexpr int marginRight = 10;
constexpr int marginBottom = 30;
constexpr int marginBottom = 22;
constexpr int marginLeft = 10;
void EpubReaderScreen::taskTrampoline(void* param) {
@@ -60,10 +60,8 @@ void EpubReaderScreen::onExit() {
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
delete section;
section = nullptr;
delete epub;
epub = nullptr;
section.reset();
epub.reset();
}
void EpubReaderScreen::handleInput() {
@@ -88,8 +86,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
updateRequired = true;
return;
@@ -109,8 +106,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -122,8 +118,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -155,7 +150,7 @@ void EpubReaderScreen::renderScreen() {
if (!section) {
const auto filepath = epub->getSpineItem(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,
marginLeft)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
@@ -167,7 +162,10 @@ void EpubReaderScreen::renderScreen() {
constexpr int y = 50;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
renderer.grayscaleRevert();
uint8_t *fb1 = renderer.getFrameBuffer();
renderer.swapBuffers();
memcpy(fb1, renderer.getFrameBuffer(), EInkDisplay::BUFFER_SIZE);
renderer.fillRect(x, y, w, h, 0);
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
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,
marginLeft)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
delete section;
section = nullptr;
section.reset();
return;
}
} else {
@@ -212,11 +209,18 @@ void EpubReaderScreen::renderScreen() {
return;
}
const Page* p = section->loadPageFromSD();
const auto start = millis();
renderContents(p);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
delete p;
{
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();
renderContents(std::move(p));
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
uint8_t data[4];
@@ -228,8 +232,8 @@ void EpubReaderScreen::renderScreen() {
f.close();
}
void EpubReaderScreen::renderContents(const Page* p) {
p->render(renderer, READER_FONT_ID);
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
page->render(renderer, READER_FONT_ID);
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@@ -244,13 +248,13 @@ void EpubReaderScreen::renderContents(const Page* p) {
{
renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
p->render(renderer, READER_FONT_ID);
page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer
renderer.clearScreen(0x00);
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
p->render(renderer, READER_FONT_ID);
page->render(renderer, READER_FONT_ID);
renderer.copyGrayscaleMsbBuffers();
// display grayscale part
@@ -260,17 +264,18 @@ void EpubReaderScreen::renderContents(const Page* p) {
}
void EpubReaderScreen::renderStatusBar() const {
constexpr auto textY = 776;
// Right aligned text for progress counter
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());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, 776,
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str());
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = std::to_string(percentage) + "%";
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
constexpr int batteryWidth = 15;
@@ -286,8 +291,8 @@ void EpubReaderScreen::renderStatusBar() const {
renderer.drawLine(x, y, x, y + batteryHeight - 1);
// Battery end
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 - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
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);
// 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());
}
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"
class EpubReaderScreen final : public Screen {
Epub* epub;
Section* section = nullptr;
std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0;
@@ -21,13 +21,13 @@ class EpubReaderScreen final : public Screen {
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderContents(const Page* p);
void renderContents(std::unique_ptr<Page> p);
void renderStatusBar() const;
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)
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void handleInput() override;