15 Commits
0.3.0 ... 0.5.1

Author SHA1 Message Date
Dave Allie
dfc74f94c2 Cut release 0.5.1 2025-12-13 21:52:48 +11:00
Dave Allie
3518cbb56d Add user guide 2025-12-13 21:52:03 +11:00
Dave Allie
8994953254 Add chapter selection screen 2025-12-13 21:17:34 +11:00
Dave Allie
ead39fd04b Return -1 from getTocIndexForSpineIndex if TOC item does not exist 2025-12-13 21:17:22 +11:00
Dave Allie
5a7381a0eb Cut release 0.5.0 2025-12-13 20:16:12 +11:00
Dave Allie
f69fc90b5c Show end of book screen when navigating past last page 2025-12-13 20:10:38 +11:00
Dave Allie
5bae283838 Process lines into pages as they are built 2025-12-13 20:10:16 +11:00
Dave Allie
c7a32fe41f Remove tinyxml2 dependency replace with expat parsers (#9) 2025-12-13 19:36:01 +11:00
Dave Allie
d450f362d1 Cut release 0.4.0 2025-12-13 17:15:06 +11:00
Dave Allie
6ddcf9b592 Show clearer indexing string 2025-12-13 16:02:27 +11:00
Dave Allie
492c6fd23e Bump page file version 2025-12-13 00:42:17 +11:00
Dave Allie
7c852cf7d1 Swap out babyblue font for pixelarial14 2025-12-13 00:16:10 +11:00
Dave Allie
69f357998e Move to smart pointers and split out ParsedText class (#6)
* Move to smart pointers and split out ParsedText class

* Cleanup ParsedText

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

* Bump Page and Section file versions

* Combine removeDir implementations in Epub

* Adjust screen margins
2025-12-12 22:13:34 +11:00
Dave Allie
09f68a3d03 Restructure readme 2025-12-09 00:07:38 +11:00
Dave Allie
7ec7efcb47 Add section in readme on flashing via xteink.dve.al 2025-12-09 00:04:55 +11:00
37 changed files with 1665 additions and 623 deletions

2
.clangd Normal file
View File

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

View File

@@ -36,7 +36,34 @@ 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.
## Usage
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
## Development
### Prerequisites
@@ -58,24 +85,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

78
USER_GUIDE.md Normal file
View File

@@ -0,0 +1,78 @@
# CrossPoint User Guide
Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of
the device.
## 1. Hardware Overview
The device utilises the standard buttons on the Xtink X4 in the same layout:
### Button Layout
| Location | Buttons |
|-----------------|--------------------------------------------|
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
---
## 2. Power & Startup
### Power On / Off
To turn the device on or off, **press and hold the Power button for 1 full second**.
### First Launch
Upon turning the device on for the first time, you will be placed on the **Book Selection Screen** (File Browser).
> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading.
---
## 3. Book Selection
The Home Screen acts as a folder and file browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up
and down through folders and books.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
---
## 4. Reading Mode
Once you have opened a book, the button layout changes to facilitate reading.
### Page Turning
| Action | Buttons |
|-------------------|--------------------------------------|
| **Previous Page** | Press **Left** _or_ **Volume Up** |
| **Next Page** | Press **Right** _or_ **Volume Down** |
### Chapter Navigation
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
### System Navigation
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen.
---
## 5. Chapter Selection Screen
Accessible by pressing **Confirm** while inside a book.
1. Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to highlight the desired chapter.
2. Press **Confirm** to jump to that chapter.
3. *Alternatively, press **Back** to cancel and return to your current page.*
---
## 6. Current Limitations & Roadmap
Please note that this firmware is currently in active development. The following features are **not yet supported** but
are planned for future updates:
* **Images:** Embedded images in e-books will not render.
* **Text Formatting:** There are currently no settings to adjust font type, size, line spacing, or margins.

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,250 +6,166 @@
#include <map>
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
// open up the meta data to find where the content.opf file lives
size_t s;
const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true));
if (!metaInfo) {
Serial.printf("[%lu] [EBP] Could not find META-INF/container.xml\n", millis());
#include "Epub/FsHelpers.h"
#include "Epub/parsers/ContainerParser.h"
#include "Epub/parsers/ContentOpfParser.h"
#include "Epub/parsers/TocNcxParser.h"
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
const auto containerPath = "META-INF/container.xml";
size_t containerSize;
// Get file size without loading it all into heap
if (!getItemSize(containerPath, &containerSize)) {
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
return false;
}
// parse the meta data
tinyxml2::XMLDocument metaDataDoc;
const auto result = metaDataDoc.Parse(metaInfo);
free(metaInfo);
ContainerParser containerParser(containerSize);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Could not parse META-INF/container.xml. Error: %d\n", millis(), result);
if (!containerParser.setup()) {
return false;
}
const auto container = metaDataDoc.FirstChildElement("container");
if (!container) {
Serial.printf("[%lu] [EBP] Could not find container element in META-INF/container.xml\n", millis());
// Stream read (reusing your existing stream logic)
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
containerParser.teardown();
return false;
}
const auto rootfiles = container->FirstChildElement("rootfiles");
if (!rootfiles) {
Serial.printf("[%lu] [EBP] Could not find rootfiles element in META-INF/container.xml\n", millis());
// Extract the result
if (containerParser.fullPath.empty()) {
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
containerParser.teardown();
return false;
}
// find the root file that has the media-type="application/oebps-package+xml"
auto rootfile = rootfiles->FirstChildElement("rootfile");
while (rootfile) {
const char* mediaType = rootfile->Attribute("media-type");
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
const char* full_path = rootfile->Attribute("full-path");
if (full_path) {
contentOpfFile = full_path;
return true;
}
}
rootfile = rootfile->NextSiblingElement("rootfile");
}
*contentOpfFile = std::move(containerParser.fullPath);
Serial.printf("[%lu] [EBP] Could not get path to content.opf file\n", millis());
return false;
}
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
// read in the content.opf file and parse it
auto contents = reinterpret_cast<char*>(zip.readFileToMemory(content_opf_file.c_str(), nullptr, true));
// parse the contents
tinyxml2::XMLDocument doc;
auto result = doc.Parse(contents);
free(contents);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Error parsing content.opf - %s\n", millis(),
tinyxml2::XMLDocument::ErrorIDToName(result));
return false;
}
auto package = doc.FirstChildElement("package");
if (!package) package = doc.FirstChildElement("opf:package");
if (!package) {
Serial.printf("[%lu] [EBP] Could not find package element in content.opf\n", millis());
return false;
}
// get the metadata - title and cover image
auto metadata = package->FirstChildElement("metadata");
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
if (!metadata) {
Serial.printf("[%lu] [EBP] Missing metadata\n", millis());
return false;
}
auto titleEl = metadata->FirstChildElement("dc:title");
if (!titleEl) {
Serial.printf("[%lu] [EBP] Missing title\n", millis());
return false;
}
this->title = titleEl->GetText();
auto cover = metadata->FirstChildElement("meta");
if (!cover) cover = metadata->FirstChildElement("opf:meta");
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
cover = cover->NextSiblingElement("meta");
}
if (!cover) {
Serial.printf("[%lu] [EBP] Missing cover\n", millis());
}
auto coverItem = cover ? cover->Attribute("content") : nullptr;
// read the manifest and spine
// the manifest gives us the names of the files
// the spine gives us the order of the files
// we can then read the files in the order they are in the spine
auto manifest = package->FirstChildElement("manifest");
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
if (!manifest) {
Serial.printf("[%lu] [EBP] Missing manifest\n", millis());
return false;
}
// create a mapping from id to file name
auto item = manifest->FirstChildElement("item");
if (!item) item = manifest->FirstChildElement("opf:item");
std::map<std::string, std::string> items;
while (item) {
std::string itemId = item->Attribute("id");
std::string href = contentBasePath + item->Attribute("href");
// grab the cover image
if (coverItem && itemId == coverItem) {
coverImageItem = href;
}
// grab the ncx file
if (itemId == "ncx" || itemId == "ncxtoc") {
tocNcxItem = href;
}
items[itemId] = href;
auto nextItem = item->NextSiblingElement("item");
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
item = nextItem;
}
// find the spine
auto spineEl = package->FirstChildElement("spine");
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
if (!spineEl) {
Serial.printf("[%lu] [EBP] Missing spine\n", millis());
return false;
}
// read the spine
auto itemref = spineEl->FirstChildElement("itemref");
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
while (itemref) {
auto id = itemref->Attribute("idref");
if (items.find(id) != items.end()) {
spine.emplace_back(id, items[id]);
}
auto nextItemRef = itemref->NextSiblingElement("itemref");
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
itemref = nextItemRef;
}
containerParser.teardown();
return true;
}
bool Epub::parseTocNcxFile(const ZipFile& zip) {
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
size_t contentOpfSize;
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
return false;
}
ContentOpfParser opfParser(getBasePath(), contentOpfSize);
if (!opfParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
return false;
}
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
opfParser.teardown();
return false;
}
// Grab data from opfParser into epub
title = opfParser.title;
if (opfParser.items.count("ncx")) {
tocNcxItem = opfParser.items.at("ncx");
} else if (opfParser.items.count("ncxtoc")) {
tocNcxItem = opfParser.items.at("ncxtoc");
}
for (auto& spineRef : opfParser.spineRefs) {
if (opfParser.items.count(spineRef)) {
spine.emplace_back(spineRef, opfParser.items.at(spineRef));
}
}
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
opfParser.teardown();
return true;
}
bool Epub::parseTocNcxFile() {
// the ncx file should have been specified in the content.opf file
if (tocNcxItem.empty()) {
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
return false;
}
const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true));
if (!ncxData) {
Serial.printf("[%lu] [EBP] Could not find %s\n", millis(), tocNcxItem.c_str());
size_t tocSize;
if (!getItemSize(tocNcxItem, &tocSize)) {
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
return false;
}
// Parse the Toc contents
tinyxml2::XMLDocument doc;
const auto result = doc.Parse(ncxData);
free(ncxData);
TocNcxParser ncxParser(contentBasePath, tocSize);
if (result != tinyxml2::XML_SUCCESS) {
Serial.printf("[%lu] [EBP] Error parsing toc %s\n", millis(), tinyxml2::XMLDocument::ErrorIDToName(result));
if (!ncxParser.setup()) {
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
return false;
}
const auto ncx = doc.FirstChildElement("ncx");
if (!ncx) {
Serial.printf("[%lu] [EBP] Could not find first child ncx in toc\n", millis());
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
ncxParser.teardown();
return false;
}
const auto navMap = ncx->FirstChildElement("navMap");
if (!navMap) {
Serial.printf("[%lu] [EBP] Could not find navMap child in ncx\n", millis());
return false;
}
this->toc = std::move(ncxParser.toc);
recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
ncxParser.teardown();
return true;
}
void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) {
// Fills toc map
while (element) {
std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
const auto content = element->FirstChildElement("content");
std::string href = contentBasePath + content->Attribute("src");
// split the href on the # to get the href and the anchor
const size_t pos = href.find('#');
std::string anchor;
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
toc.emplace_back(navTitle, href, anchor, 0);
tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint");
if (nestedNavPoint) {
recursivelyParseNavMap(nestedNavPoint);
}
element = element->NextSiblingElement("navPoint");
}
}
// load in the meta data for the epub file
bool Epub::load() {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
ZipFile zip("/sd" + filepath);
std::string contentOpfFile;
if (!findContentOpfFile(zip, contentOpfFile)) {
Serial.printf("[%lu] [EBP] Could not open ePub\n", millis());
std::string contentOpfFilePath;
if (!findContentOpfFile(&contentOpfFilePath)) {
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
return false;
}
contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1);
Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str());
if (!parseContentOpf(zip, contentOpfFile)) {
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
if (!parseContentOpf(contentOpfFilePath)) {
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
return false;
}
if (!parseTocNcxFile(zip)) {
if (!parseTocNcxFile()) {
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
return false;
}
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true;
}
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
bool Epub::clearCache() const {
if (!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())) {
@@ -329,6 +245,13 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
return zip.readFileToStream(path.c_str(), out, chunkSize);
}
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
const ZipFile zip("/sd" + filepath);
const std::string path = normalisePath(itemHref);
return zip.getInflatedFileSize(path.c_str(), size);
}
int Epub::getSpineItemsCount() const { return spine.size(); }
std::string& Epub::getSpineItem(const int spineIndex) {
@@ -376,6 +299,5 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
}
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
// not found - default to first item
return 0;
return -1;
}

View File

@@ -1,22 +1,13 @@
#pragma once
#include <Print.h>
#include <tinyxml2.h>
#include <string>
#include <unordered_map>
#include <vector>
class ZipFile;
#include "Epub/EpubTocEntry.h"
class EpubTocEntry {
public:
std::string title;
std::string href;
std::string anchor;
int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
};
class ZipFile;
class Epub {
// the title read from the EPUB meta data
@@ -36,11 +27,9 @@ class Epub {
// Uniq cache key based on filepath
std::string cachePath;
// find the path for the content.opf file
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
bool parseTocNcxFile(const ZipFile& zip);
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(const std::string& contentOpfFilePath);
bool parseTocNcxFile();
public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@@ -50,7 +39,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;
@@ -59,6 +48,7 @@ class Epub {
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
bool getItemSize(const std::string& itemHref, size_t* size) const;
std::string& getSpineItem(int spineIndex);
int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex);

View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
class EpubTocEntry {
public:
std::string title;
std::string href;
std::string anchor;
int level;
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
};

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,164 @@
#include "ParsedText.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cmath>
#include <functional>
#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
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
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;
}
// 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);
processLine(
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;
}
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <EpdFontFamily.h>
#include <cstdint>
#include <functional>
#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(); }
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
};

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"
#include "parsers/ChapterHtmlSlimParser.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); });
ChapterHtmlSlimParser 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,15 +1,15 @@
#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,
};
// represents a block of words in the html document
class TextBlock final : public Block {
public:
enum BLOCK_STYLE : uint8_t {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
@@ -17,34 +17,24 @@ enum BLOCK_STYLE : uint8_t {
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;
// 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

@@ -1,11 +1,11 @@
#include "EpubHtmlParserSlim.h"
#include "ChapterHtmlSlimParser.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <expat.h>
#include "Page.h"
#include "htmlEntities.h"
#include "../Page.h"
#include "../htmlEntities.h"
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
@@ -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 ChapterHtmlSlimParser::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,15 +46,13 @@ 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) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
(void)atts;
// Middle of skip
@@ -64,23 +62,7 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// const char* src = element.Attribute("src");
// if (src) {
// // don't leave an empty text block in the list
// // const BLOCK_STYLE style = currentTextBlock->get_style();
// if (currentTextBlock->isEmpty()) {
// delete currentTextBlock;
// currentTextBlock = nullptr;
// }
// // TODO: Fix this
// // blocks.push_back(new ImageBlock(m_base_path + src));
// // start a new text block - with the same style as before
// // startNewTextBlock(style);
// } else {
// // ESP_LOGE(TAG, "Could not find src attribute");
// }
// start skip
// TODO: Start processing image tags
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
@@ -94,13 +76,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);
@@ -111,21 +93,29 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
self->depth += 1;
}
void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
// Middle of skip
if (self->skipUntilDepth < self->depth) {
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 +125,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;
}
@@ -144,8 +133,8 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
}
}
void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
(void)name;
if (self->partWordBufferIndex > 0) {
@@ -158,9 +147,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;
}
}
@@ -183,8 +180,8 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
}
}
bool EpubHtmlParserSlim::parseAndBuildPages() {
startNewTextBlock(JUSTIFIED);
bool ChapterHtmlSlimParser::parseAndBuildPages() {
startNewTextBlock(TextBlock::JUSTIFIED);
const XML_Parser parser = XML_ParserCreate(nullptr);
int done;
@@ -240,56 +237,43 @@ 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;
}
void EpubHtmlParserSlim::makePages() {
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
if (currentPageNextY + lineHeight > pageHeight) {
completePageFn(std::move(currentPage));
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
currentPage->elements.push_back(std::make_shared<PageLine>(line, marginLeft, currentPageNextY));
currentPageNextY += lineHeight;
}
void ChapterHtmlSlimParser::makePages() {
if (!currentTextBlock) {
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
return;
}
if (!currentPage) {
currentPage = new Page();
currentPage.reset(new Page());
currentPageNextY = marginTop;
}
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom;
// 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);
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;
}
// add some extra line between blocks
currentTextBlock->layoutAndExtractLines(
renderer, fontId, marginLeft + marginRight,
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
// Extra paragrpah spacing
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,18 +4,20 @@
#include <climits>
#include <functional>
#include <memory>
#include "blocks/TextBlock.h"
#include "../ParsedText.h"
#include "../blocks/TextBlock.h"
class Page;
class GfxRenderer;
#define MAX_WORD_SIZE 200
class EpubHtmlParserSlim {
class ChapterHtmlSlimParser {
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);
@@ -42,10 +44,10 @@ class EpubHtmlParserSlim {
static void XMLCALL endElement(void* userData, const XML_Char* name);
public:
explicit EpubHtmlParserSlim(const char* filepath, GfxRenderer& renderer, const int fontId,
explicit ChapterHtmlSlimParser(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),
@@ -55,6 +57,7 @@ class EpubHtmlParserSlim {
marginBottom(marginBottom),
marginLeft(marginLeft),
completePageFn(completePageFn) {}
~EpubHtmlParserSlim() = default;
~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line);
};

View File

@@ -0,0 +1,96 @@
#include "ContainerParser.h"
#include <HardwareSerial.h>
bool ContainerParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [CTR] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
return true;
}
bool ContainerParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [CTR] Couldn't allocate buffer\n", millis());
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [CTR] Parse error: %s\n", millis(), XML_ErrorString(XML_GetErrorCode(parser)));
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ContainerParser*>(userData);
// Simple state tracking to ensure we are looking at the valid schema structure
if (self->state == START && strcmp(name, "container") == 0) {
self->state = IN_CONTAINER;
return;
}
if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) {
self->state = IN_ROOTFILES;
return;
}
if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) {
const char* mediaType = nullptr;
const char* path = nullptr;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "media-type") == 0) {
mediaType = atts[i + 1];
} else if (strcmp(atts[i], "full-path") == 0) {
path = atts[i + 1];
}
}
// Check if this is the standard OEBPS package
if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) {
self->fullPath = path;
}
}
}
void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ContainerParser*>(userData);
if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) {
self->state = IN_CONTAINER;
} else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) {
self->state = START;
}
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include <Print.h>
#include <string>
#include "expat.h"
class ContainerParser final : public Print {
enum ParserState {
START,
IN_CONTAINER,
IN_ROOTFILES,
};
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void endElement(void* userData, const XML_Char* name);
public:
std::string fullPath;
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};

View File

@@ -0,0 +1,161 @@
#include "ContentOpfParser.h"
#include <HardwareSerial.h>
#include <ZipFile.h>
bool ContentOpfParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
return true;
}
bool ContentOpfParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [COF] Couldn't allocate memory for buffer\n", millis());
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [COF] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ContentOpfParser*>(userData);
(void)atts;
if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
self->state = IN_METADATA;
return;
}
if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) {
self->state = IN_BOOK_TITLE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_MANIFEST;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
self->state = IN_SPINE;
return;
}
// TODO: Support book cover
// if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
// }
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
std::string itemId;
std::string href;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "id") == 0) {
itemId = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) {
href = self->baseContentPath + atts[i + 1];
}
}
self->items[itemId] = href;
return;
}
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "idref") == 0) {
self->spineRefs.emplace_back(atts[i + 1]);
break;
}
}
return;
}
}
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<ContentOpfParser*>(userData);
if (self->state == IN_BOOK_TITLE) {
self->title.append(s, len);
return;
}
}
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<ContentOpfParser*>(userData);
(void)name;
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) {
self->state = IN_METADATA;
return;
}
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
self->state = IN_PACKAGE;
return;
}
if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
self->state = START;
return;
}
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <Print.h>
#include <map>
#include "Epub.h"
#include "expat.h"
class ContentOpfParser final : public Print {
enum ParserState {
START,
IN_PACKAGE,
IN_METADATA,
IN_BOOK_TITLE,
IN_MANIFEST,
IN_SPINE,
};
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
public:
std::string title;
std::string tocNcxPath;
std::map<std::string, std::string> items;
std::vector<std::string> spineRefs;
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};

View File

@@ -0,0 +1,165 @@
#include "TocNcxParser.h"
#include <HardwareSerial.h>
bool TocNcxParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for parser\n", millis());
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
return true;
}
bool TocNcxParser::teardown() {
if (parser) {
XML_ParserFree(parser);
parser = nullptr;
}
return true;
}
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
Serial.printf("[%lu] [TOC] Couldn't allocate memory for buffer\n", millis());
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [TOC] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
// NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine:
// <navPoint>
// <navLabel><text>Chapter 1</text></navLabel>
// <content src="ch1.html"/>
// <navPoint> ...nested... </navPoint>
// </navPoint>
//
// This will NOT:
// <navPoint>
// <navPoint> ...nested... </navPoint>
// <navLabel><text>Chapter 1</text></navLabel>
// <content src="ch1.html"/>
// </navPoint>
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == START && strcmp(name, "ncx") == 0) {
self->state = IN_NCX;
return;
}
if (self->state == IN_NCX && strcmp(name, "navMap") == 0) {
self->state = IN_NAV_MAP;
return;
}
// Handles both top-level and nested navPoints
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
self->state = IN_NAV_POINT;
self->currentDepth++;
self->currentLabel.clear();
self->currentSrc.clear();
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) {
self->state = IN_NAV_LABEL;
return;
}
if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) {
self->state = IN_NAV_LABEL_TEXT;
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "src") == 0) {
self->currentSrc = atts[i + 1];
break;
}
}
return;
}
}
void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == IN_NAV_LABEL_TEXT) {
self->currentLabel.append(s, len);
}
}
void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) {
self->state = IN_NAV_LABEL;
return;
}
if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) {
self->state = IN_NAV_POINT;
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) {
self->currentDepth--;
if (self->currentDepth == 0) {
self->state = IN_NAV_MAP;
}
return;
}
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
// At this point (end of content tag), we likely have both Label (from previous tags) and Src.
// This is the safest place to push the data, assuming <navLabel> always comes before <content>.
// NCX spec says navLabel comes before content.
if (!self->currentLabel.empty() && !self->currentSrc.empty()) {
std::string href = self->baseContentPath + self->currentSrc;
std::string anchor;
const size_t pos = href.find('#');
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
// Push to vector
self->toc.emplace_back(self->currentLabel, href, anchor, self->currentDepth);
// Clear them so we don't re-add them if there are weird XML structures
self->currentLabel.clear();
self->currentSrc.clear();
}
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <Print.h>
#include <string>
#include <vector>
#include "Epub/EpubTocEntry.h"
#include "expat.h"
class TocNcxParser final : public Print {
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
ParserState state = START;
std::string currentLabel;
std::string currentSrc;
size_t currentDepth = 0;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
public:
std::vector<EpubTocEntry> toc;
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
bool setup();
bool teardown();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
};

View File

@@ -162,8 +162,12 @@ 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

@@ -40,7 +40,7 @@ bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileS
// find the file
mz_uint32 fileIndex = 0;
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis, filename);
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
mz_zip_reader_end(&zipArchive);
return false;
}
@@ -82,6 +82,16 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
return fileOffset + localHeaderSize + filenameLength + extraOffset;
}
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
return false;
}
*size = static_cast<size_t>(fileStat.m_uncomp_size);
return true;
}
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) const {
mz_zip_archive_file_stat fileStat;
if (!loadFileStat(filename, &fileStat)) {
@@ -268,7 +278,14 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
// Write output chunk
if (outBytes > 0) {
processedOutputBytes += outBytes;
out.write(outputBuffer + outputCursor, outBytes);
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
fclose(file);
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
}
// Update output position in buffer (with wraparound)
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
}

View File

@@ -14,6 +14,7 @@ class ZipFile {
public:
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
~ZipFile() = default;
bool getInflatedFileSize(const char* filename, size_t* size) const;
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;
};

View File

@@ -1,5 +1,5 @@
[platformio]
crosspoint_version = 0.3.0
crosspoint_version = 0.5.1
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
@@ -28,7 +29,6 @@ board_build.partitions = partitions.csv
; Libraries
lib_deps =
https://github.com/leethomason/tinyxml2.git#11.0.0
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay

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

@@ -0,0 +1,107 @@
#include "EpubReaderChapterSelectionScreen.h"
#include <GfxRenderer.h>
#include <SD.h>
#include "config.h"
constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700;
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param);
self->displayTaskLoop();
}
void EpubReaderChapterSelectionScreen::onEnter() {
if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = currentSpineIndex;
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderChapterSelectionScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionScreen::handleInput() {
const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased =
inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
onSelectSpineIndex(selectorIndex);
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
} else if (prevReleased) {
if (skipPage) {
selectorIndex =
((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + epub->getSpineItemsCount()) % epub->getSpineItemsCount();
} else {
selectorIndex = (selectorIndex + epub->getSpineItemsCount() - 1) % epub->getSpineItemsCount();
}
updateRequired = true;
} else if (nextReleased) {
if (skipPage) {
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % epub->getSpineItemsCount();
} else {
selectorIndex = (selectorIndex + 1) % epub->getSpineItemsCount();
}
updateRequired = true;
}
}
void EpubReaderChapterSelectionScreen::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderChapterSelectionScreen::renderScreen() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(READER_FONT_ID, 10, "Select Chapter", true, BOLD);
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30);
for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + PAGE_ITEMS; i++) {
const int tocIndex = epub->getTocIndexForSpineIndex(i);
if (tocIndex == -1) {
renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, "Unnamed", i != selectorIndex);
} else {
auto item = epub->getTocItem(tocIndex);
renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(),
i != selectorIndex);
}
}
renderer.displayBuffer();
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory>
#include "Screen.h"
class EpubReaderChapterSelectionScreen final : public Screen {
std::shared_ptr<Epub> epub;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Screen(renderer, inputManager),
epub(epub),
currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};

View File

@@ -5,14 +5,15 @@
#include <SD.h>
#include "Battery.h"
#include "EpubReaderChapterSelectionScreen.h"
#include "config.h"
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,13 +61,42 @@ void EpubReaderScreen::onExit() {
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
delete section;
section = nullptr;
delete epub;
epub = nullptr;
section.reset();
epub.reset();
}
void EpubReaderScreen::handleInput() {
// Pass input responsibility to sub screen if exists
if (subScreen) {
subScreen->handleInput();
return;
}
// Enter chapter selection screen
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start screen transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
subScreen.reset(new EpubReaderChapterSelectionScreen(
this->renderer, this->inputManager, epub, currentSpineIndex,
[this] {
subScreen->onExit();
subScreen.reset();
updateRequired = true;
},
[this](const int newSpineIndex) {
if (currentSpineIndex != newSpineIndex) {
currentSpineIndex = newSpineIndex;
nextPageNumber = 0;
section.reset();
}
subScreen->onExit();
subScreen.reset();
updateRequired = true;
}));
subScreen->onEnter();
xSemaphoreGive(renderingMutex);
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoHome();
return;
@@ -81,6 +111,14 @@ void EpubReaderScreen::handleInput() {
return;
}
// any botton press when at end of the book goes back to the last page
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
updateRequired = true;
return;
}
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
if (skipChapter) {
@@ -88,8 +126,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 +146,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = UINT16_MAX;
currentSpineIndex--;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -122,8 +158,7 @@ void EpubReaderScreen::handleInput() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
nextPageNumber = 0;
currentSpineIndex++;
delete section;
section = nullptr;
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
@@ -148,14 +183,27 @@ void EpubReaderScreen::renderScreen() {
return;
}
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
// edge case handling for sub-zero spine index
if (currentSpineIndex < 0) {
currentSpineIndex = 0;
}
// based bounds of book, show end of book screen
if (currentSpineIndex > epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount();
}
// Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen();
renderer.drawCenteredText(READER_FONT_ID, 300, "End of book", true, BOLD);
renderer.displayBuffer();
return;
}
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 +215,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 +230,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 +262,18 @@ void EpubReaderScreen::renderScreen() {
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();
renderContents(p);
renderContents(std::move(p));
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);
uint8_t data[4];
@@ -228,8 +285,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 +301,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 +317,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 +344,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
@@ -302,13 +360,22 @@ void EpubReaderScreen::renderStatusBar() const {
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
const int titleMarginRight = progressTextWidth + 30 + marginRight;
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
auto title = tocItem.title;
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
std::string title;
int titleWidth;
if (tocIndex == -1) {
title = "Unnamed";
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
} else {
const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title;
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth) {
title = title.substr(0, title.length() - 8) + "...";
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,10 +8,11 @@
#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;
std::unique_ptr<Screen> subScreen = nullptr;
int currentSpineIndex = 0;
int nextPageNumber = 0;
int pagesUntilFullRefresh = 0;
@@ -21,13 +22,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;