Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc74f94c2 | ||
|
|
3518cbb56d | ||
|
|
8994953254 | ||
|
|
ead39fd04b | ||
|
|
5a7381a0eb | ||
|
|
f69fc90b5c | ||
|
|
5bae283838 | ||
|
|
c7a32fe41f | ||
|
|
d450f362d1 | ||
|
|
6ddcf9b592 | ||
|
|
492c6fd23e | ||
|
|
7c852cf7d1 | ||
|
|
69f357998e | ||
|
|
09f68a3d03 | ||
|
|
7ec7efcb47 | ||
|
|
45af2d0e81 | ||
|
|
0926e9e6e4 | ||
|
|
02b157c02b | ||
|
|
07cc589e59 | ||
|
|
b743a1ca8e | ||
|
|
2ed8017aa2 |
@@ -36,7 +36,34 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
|||||||
- [ ] WiFi connectivity
|
- [ ] WiFi connectivity
|
||||||
- [ ] BLE connectivity
|
- [ ] BLE connectivity
|
||||||
|
|
||||||
## Getting Started
|
## Installing
|
||||||
|
|
||||||
|
### Web (latest firmware)
|
||||||
|
|
||||||
|
1. Connect your Xteink X4 to your computer via USB-C
|
||||||
|
2. Go to https://xteink.dve.al/ and click "Flash CrossPoint firmware"
|
||||||
|
|
||||||
|
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||||
|
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||||
|
|
||||||
|
### Web (specific firmware version)
|
||||||
|
|
||||||
|
1. Connect your Xteink X4 to your computer via USB-C
|
||||||
|
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
|
||||||
|
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
||||||
|
|
||||||
|
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||||
|
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
See [Development](#development) below.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@@ -58,24 +85,12 @@ git submodule update --init --recursive
|
|||||||
|
|
||||||
### Flashing your device
|
### Flashing your device
|
||||||
|
|
||||||
#### Command line
|
|
||||||
|
|
||||||
Connect your Xteink X4 to your computer via USB-C and run the following command.
|
Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pio run --target upload
|
pio run --target upload
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Web
|
|
||||||
|
|
||||||
1. Connect your Xteink X4 to your computer via USB-C
|
|
||||||
2. Download the `firmware.bin` file from the latest release via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
|
|
||||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
|
||||||
4. Press the reset button on the Xteink X4 to restart the device
|
|
||||||
|
|
||||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
|
||||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -31,4 +31,5 @@ typedef struct {
|
|||||||
uint8_t advanceY; ///< Newline distance (y axis)
|
uint8_t advanceY; ///< Newline distance (y axis)
|
||||||
int ascender; ///< Maximal height of a glyph above the base line
|
int ascender; ///< Maximal height of a glyph above the base line
|
||||||
int descender; ///< Maximal height of a glyph below the base line
|
int descender; ///< Maximal height of a glyph below the base line
|
||||||
|
bool is2Bit;
|
||||||
} EpdFontData;
|
} EpdFontData;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: babyblue
|
* name: babyblue
|
||||||
* size: 8
|
* size: 8
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@@ -500,5 +501,5 @@ static const EpdUnicodeInterval babyblueIntervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData babyblue = {
|
static const EpdFontData babyblue = {
|
||||||
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4,
|
babyblueBitmaps, babyblueGlyphs, babyblueIntervals, 5, 17, 13, -4, false,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: ubuntu_10
|
* name: ubuntu_10
|
||||||
* size: 10
|
* size: 10
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@@ -762,5 +763,5 @@ static const EpdUnicodeInterval ubuntu_10Intervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData ubuntu_10 = {
|
static const EpdFontData ubuntu_10 = {
|
||||||
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4,
|
ubuntu_10Bitmaps, ubuntu_10Glyphs, ubuntu_10Intervals, 31, 24, 20, -4, false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* generated by fontconvert.py
|
* generated by fontconvert.py
|
||||||
* name: ubuntu_bold_10
|
* name: ubuntu_bold_10
|
||||||
* size: 10
|
* size: 10
|
||||||
|
* mode: 1-bit
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFontData.h"
|
#include "EpdFontData.h"
|
||||||
@@ -806,5 +807,5 @@ static const EpdUnicodeInterval ubuntu_bold_10Intervals[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EpdFontData ubuntu_bold_10 = {
|
static const EpdFontData ubuntu_bold_10 = {
|
||||||
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4,
|
ubuntu_bold_10Bitmaps, ubuntu_bold_10Glyphs, ubuntu_bold_10Intervals, 31, 24, 20, -4, false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ parser = argparse.ArgumentParser(description="Generate a header file from a font
|
|||||||
parser.add_argument("name", action="store", help="name of the font.")
|
parser.add_argument("name", action="store", help="name of the font.")
|
||||||
parser.add_argument("size", type=int, help="font size to use.")
|
parser.add_argument("size", type=int, help="font size to use.")
|
||||||
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
||||||
|
parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
|
||||||
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
|
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
|
||||||
|
|
||||||
font_stack = [freetype.Face(f) for f in args.fontstack]
|
font_stack = [freetype.Face(f) for f in args.fontstack]
|
||||||
|
is2Bit = args.is2Bit
|
||||||
size = args.size
|
size = args.size
|
||||||
font_name = args.name
|
font_name = args.name
|
||||||
|
|
||||||
@@ -173,26 +175,73 @@ for i_start, i_end in intervals:
|
|||||||
pixels4g.append(px)
|
pixels4g.append(px)
|
||||||
px = 0
|
px = 0
|
||||||
|
|
||||||
# Downsample to 1-bit bitmap - treat any non-zero as black
|
if is2Bit:
|
||||||
pixelsbw = []
|
# 0 = white, 15 black, 8+ dark grey, 7- light grey
|
||||||
px = 0
|
# Downsample to 2-bit bitmap
|
||||||
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
pixels2b = []
|
||||||
for y in range(bitmap.rows):
|
px = 0
|
||||||
for x in range(bitmap.width):
|
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
||||||
px = px << 1
|
for y in range(bitmap.rows):
|
||||||
bm = pixels4g[y * pitch + (x // 2)]
|
for x in range(bitmap.width):
|
||||||
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
|
px = px << 2
|
||||||
|
bm = pixels4g[y * pitch + (x // 2)]
|
||||||
|
bm = (bm >> ((x % 2) * 4)) & 0xF
|
||||||
|
|
||||||
if (y * bitmap.width + x) % 8 == 7:
|
if bm == 15:
|
||||||
pixelsbw.append(px)
|
px += 3
|
||||||
px = 0
|
elif bm >= 8:
|
||||||
if (bitmap.width * bitmap.rows) % 8 != 0:
|
px += 2
|
||||||
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
elif bm > 0:
|
||||||
pixelsbw.append(px)
|
px += 1
|
||||||
|
|
||||||
|
if (y * bitmap.width + x) % 4 == 3:
|
||||||
|
pixels2b.append(px)
|
||||||
|
px = 0
|
||||||
|
if (bitmap.width * bitmap.rows) % 4 != 0:
|
||||||
|
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
|
||||||
|
pixels2b.append(px)
|
||||||
|
|
||||||
|
# for y in range(bitmap.rows):
|
||||||
|
# line = ''
|
||||||
|
# for x in range(bitmap.width):
|
||||||
|
# pixelPosition = y * bitmap.width + x
|
||||||
|
# byte = pixels2b[pixelPosition // 4]
|
||||||
|
# bit_index = (3 - (pixelPosition % 4)) * 2
|
||||||
|
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
|
||||||
|
# print(line)
|
||||||
|
# print('')
|
||||||
|
else:
|
||||||
|
# Downsample to 1-bit bitmap - treat any non-zero as black
|
||||||
|
pixelsbw = []
|
||||||
|
px = 0
|
||||||
|
pitch = (bitmap.width // 2) + (bitmap.width % 2)
|
||||||
|
for y in range(bitmap.rows):
|
||||||
|
for x in range(bitmap.width):
|
||||||
|
px = px << 1
|
||||||
|
bm = pixels4g[y * pitch + (x // 2)]
|
||||||
|
px += 1 if ((x & 1) == 0 and bm & 0xF > 0) or ((x & 1) == 1 and bm & 0xF0 > 0) else 0
|
||||||
|
|
||||||
|
if (y * bitmap.width + x) % 8 == 7:
|
||||||
|
pixelsbw.append(px)
|
||||||
|
px = 0
|
||||||
|
if (bitmap.width * bitmap.rows) % 8 != 0:
|
||||||
|
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
||||||
|
pixelsbw.append(px)
|
||||||
|
|
||||||
|
# for y in range(bitmap.rows):
|
||||||
|
# line = ''
|
||||||
|
# for x in range(bitmap.width):
|
||||||
|
# pixelPosition = y * bitmap.width + x
|
||||||
|
# byte = pixelsbw[pixelPosition // 8]
|
||||||
|
# bit_index = 7 - (pixelPosition % 8)
|
||||||
|
# line += '#' if (byte >> bit_index) & 1 else '.'
|
||||||
|
# print(line)
|
||||||
|
# print('')
|
||||||
|
|
||||||
|
pixels = pixels2b if is2Bit else pixelsbw
|
||||||
|
|
||||||
# Build output data
|
# Build output data
|
||||||
packed = bytes(pixelsbw)
|
packed = bytes(pixels)
|
||||||
glyph = GlyphProps(
|
glyph = GlyphProps(
|
||||||
width = bitmap.width,
|
width = bitmap.width,
|
||||||
height = bitmap.rows,
|
height = bitmap.rows,
|
||||||
@@ -216,7 +265,7 @@ for index, glyph in enumerate(all_glyphs):
|
|||||||
glyph_data.extend([b for b in packed])
|
glyph_data.extend([b for b in packed])
|
||||||
glyph_props.append(props)
|
glyph_props.append(props)
|
||||||
|
|
||||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n */")
|
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
|
||||||
print("#pragma once")
|
print("#pragma once")
|
||||||
print("#include \"EpdFontData.h\"\n")
|
print("#include \"EpdFontData.h\"\n")
|
||||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||||
@@ -244,4 +293,5 @@ print(f" {len(intervals)},")
|
|||||||
print(f" {norm_ceil(face.size.height)},")
|
print(f" {norm_ceil(face.size.height)},")
|
||||||
print(f" {norm_ceil(face.size.ascender)},")
|
print(f" {norm_ceil(face.size.ascender)},")
|
||||||
print(f" {norm_floor(face.size.descender)},")
|
print(f" {norm_floor(face.size.descender)},")
|
||||||
|
print(f" {'true' if is2Bit else 'false'},")
|
||||||
print("};")
|
print("};")
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <EpdFontFamily.h>
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <Utf8.h>
|
|
||||||
|
|
||||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
|
||||||
inline int max(const int a, const int b) { return a > b ? a : b; }
|
|
||||||
|
|
||||||
template <typename Renderable>
|
|
||||||
class EpdFontRenderer {
|
|
||||||
Renderable& renderer;
|
|
||||||
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color, EpdFontStyle style = REGULAR);
|
|
||||||
|
|
||||||
public:
|
|
||||||
const EpdFontFamily* fontFamily;
|
|
||||||
explicit EpdFontRenderer(const EpdFontFamily* fontFamily, Renderable& renderer)
|
|
||||||
: fontFamily(fontFamily), renderer(renderer) {}
|
|
||||||
~EpdFontRenderer() = default;
|
|
||||||
void renderString(const char* string, int* x, int* y, uint16_t color, EpdFontStyle style = REGULAR);
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename Renderable>
|
|
||||||
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color,
|
|
||||||
const EpdFontStyle style) {
|
|
||||||
// cannot draw a NULL / empty string
|
|
||||||
if (string == nullptr || *string == '\0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no printable characters
|
|
||||||
if (!fontFamily->hasPrintableChars(string, style)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t cp;
|
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
|
||||||
renderChar(cp, x, y, color, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
*y += fontFamily->getData(style)->advanceY;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Renderable>
|
|
||||||
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color,
|
|
||||||
const EpdFontStyle style) {
|
|
||||||
const EpdGlyph* glyph = fontFamily->getGlyph(cp, style);
|
|
||||||
if (!glyph) {
|
|
||||||
// TODO: Replace with fallback glyph property?
|
|
||||||
glyph = fontFamily->getGlyph('?', style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no glyph?
|
|
||||||
if (!glyph) {
|
|
||||||
Serial.printf("No glyph for codepoint %d\n", cp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint32_t offset = glyph->dataOffset;
|
|
||||||
const uint8_t width = glyph->width;
|
|
||||||
const uint8_t height = glyph->height;
|
|
||||||
const int left = glyph->left;
|
|
||||||
|
|
||||||
const uint8_t* bitmap = nullptr;
|
|
||||||
bitmap = &fontFamily->getData(style)->bitmap[offset];
|
|
||||||
|
|
||||||
if (bitmap != nullptr) {
|
|
||||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
||||||
int screenY = *y - glyph->top + glyphY;
|
|
||||||
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
||||||
const int pixelPosition = glyphY * width + glyphX;
|
|
||||||
int screenX = *x + left + glyphX;
|
|
||||||
|
|
||||||
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
||||||
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
||||||
|
|
||||||
if ((byte >> bit_index) & 1) {
|
|
||||||
renderer.drawPixel(screenX, screenY, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*x += glyph->advanceX;
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#include "EpdRenderer.h"
|
|
||||||
|
|
||||||
#include "builtinFonts/babyblue.h"
|
|
||||||
#include "builtinFonts/bookerly.h"
|
|
||||||
#include "builtinFonts/bookerly_bold.h"
|
|
||||||
#include "builtinFonts/bookerly_bold_italic.h"
|
|
||||||
#include "builtinFonts/bookerly_italic.h"
|
|
||||||
#include "builtinFonts/ubuntu_10.h"
|
|
||||||
#include "builtinFonts/ubuntu_bold_10.h"
|
|
||||||
|
|
||||||
EpdFont bookerlyFont(&bookerly);
|
|
||||||
EpdFont bookerlyBoldFont(&bookerly_bold);
|
|
||||||
EpdFont bookerlyItalicFont(&bookerly_italic);
|
|
||||||
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic);
|
|
||||||
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
|
||||||
|
|
||||||
EpdFont smallFont(&babyblue);
|
|
||||||
EpdFontFamily smallFontFamily(&smallFont);
|
|
||||||
|
|
||||||
EpdFont ubuntu10Font(&ubuntu_10);
|
|
||||||
EpdFont ununtuBold10Font(&ubuntu_bold_10);
|
|
||||||
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ununtuBold10Font);
|
|
||||||
|
|
||||||
EpdRenderer::EpdRenderer(XteinkDisplay& display)
|
|
||||||
: display(display), marginTop(11), marginBottom(30), marginLeft(10), marginRight(10), lineCompression(0.95f) {
|
|
||||||
this->regularFontRenderer = new EpdFontRenderer<XteinkDisplay>(&bookerlyFontFamily, display);
|
|
||||||
this->smallFontRenderer = new EpdFontRenderer<XteinkDisplay>(&smallFontFamily, display);
|
|
||||||
this->uiFontRenderer = new EpdFontRenderer<XteinkDisplay>(&ubuntuFontFamily, display);
|
|
||||||
}
|
|
||||||
|
|
||||||
EpdRenderer::~EpdRenderer() {
|
|
||||||
delete regularFontRenderer;
|
|
||||||
delete smallFontRenderer;
|
|
||||||
delete uiFontRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpdRenderer::getTextWidth(const char* text, const EpdFontStyle style) const {
|
|
||||||
int w = 0, h = 0;
|
|
||||||
|
|
||||||
regularFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
|
||||||
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpdRenderer::getUiTextWidth(const char* text, const EpdFontStyle style) const {
|
|
||||||
int w = 0, h = 0;
|
|
||||||
|
|
||||||
uiFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
|
||||||
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpdRenderer::getSmallTextWidth(const char* text, const EpdFontStyle style) const {
|
|
||||||
int w = 0, h = 0;
|
|
||||||
|
|
||||||
smallFontRenderer->fontFamily->getTextDimensions(text, &w, &h, style);
|
|
||||||
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawText(const int x, const int y, const char* text, const uint16_t color,
|
|
||||||
const EpdFontStyle style) const {
|
|
||||||
int ypos = y + getLineHeight() + marginTop;
|
|
||||||
int xpos = x + marginLeft;
|
|
||||||
regularFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawUiText(const int x, const int y, const char* text, const uint16_t color,
|
|
||||||
const EpdFontStyle style) const {
|
|
||||||
int ypos = y + uiFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
|
||||||
int xpos = x + marginLeft;
|
|
||||||
uiFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color,
|
|
||||||
const EpdFontStyle style) const {
|
|
||||||
int ypos = y + smallFontRenderer->fontFamily->getData(style)->advanceY + marginTop;
|
|
||||||
int xpos = x + marginLeft;
|
|
||||||
smallFontRenderer->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
|
||||||
const EpdFontStyle style) const {
|
|
||||||
const size_t length = text.length();
|
|
||||||
// fit the text into the box
|
|
||||||
int start = 0;
|
|
||||||
int end = 1;
|
|
||||||
int ypos = 0;
|
|
||||||
while (true) {
|
|
||||||
if (end >= length) {
|
|
||||||
drawText(x, y + ypos, text.substr(start, length - start).c_str(), 1, style);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ypos + getLineHeight() >= height) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text[end - 1] == '\n') {
|
|
||||||
drawText(x, y + ypos, text.substr(start, end - start).c_str(), 1, style);
|
|
||||||
ypos += getLineHeight();
|
|
||||||
start = end;
|
|
||||||
end = start + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getTextWidth(text.substr(start, end - start).c_str(), style) > width) {
|
|
||||||
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), 1, style);
|
|
||||||
ypos += getLineHeight();
|
|
||||||
start = end - 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
|
|
||||||
display.drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
|
|
||||||
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
|
||||||
display.drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
|
||||||
display.fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
|
||||||
display.drawCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::fillCircle(const int x, const int y, const int radius, const uint16_t color) const {
|
|
||||||
display.fillCircle(x + marginLeft, y + marginTop, radius, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
|
||||||
const bool invert, const bool mirrorY) const {
|
|
||||||
drawImageNoMargin(bitmap, x + marginLeft, y + marginTop, width, height, invert, mirrorY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::drawImageNoMargin(const uint8_t bitmap[], const int x, const int y, const int width, const int height,
|
|
||||||
const bool invert, const bool mirrorY) const {
|
|
||||||
display.drawImage(bitmap, x, y, width, height, invert, mirrorY);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::clearScreen(const bool black) const {
|
|
||||||
Serial.println("Clearing screen");
|
|
||||||
display.fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpdRenderer::flushDisplay(const bool partialUpdate) const { display.display(partialUpdate); }
|
|
||||||
|
|
||||||
void EpdRenderer::flushArea(const int x, const int y, const int width, const int height) const {
|
|
||||||
display.displayWindow(x + marginLeft, y + marginTop, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpdRenderer::getPageWidth() const { return display.width() - marginLeft - marginRight; }
|
|
||||||
|
|
||||||
int EpdRenderer::getPageHeight() const { return display.height() - marginTop - marginBottom; }
|
|
||||||
|
|
||||||
int EpdRenderer::getSpaceWidth() const { return regularFontRenderer->fontFamily->getGlyph(' ', REGULAR)->advanceX; }
|
|
||||||
|
|
||||||
int EpdRenderer::getLineHeight() const {
|
|
||||||
return regularFontRenderer->fontFamily->getData(REGULAR)->advanceY * lineCompression;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <GxEPD2_BW.h>
|
|
||||||
|
|
||||||
#include <EpdFontRenderer.hpp>
|
|
||||||
|
|
||||||
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
|
|
||||||
|
|
||||||
class EpdRenderer {
|
|
||||||
XteinkDisplay& display;
|
|
||||||
EpdFontRenderer<XteinkDisplay>* regularFontRenderer;
|
|
||||||
EpdFontRenderer<XteinkDisplay>* smallFontRenderer;
|
|
||||||
EpdFontRenderer<XteinkDisplay>* uiFontRenderer;
|
|
||||||
int marginTop;
|
|
||||||
int marginBottom;
|
|
||||||
int marginLeft;
|
|
||||||
int marginRight;
|
|
||||||
float lineCompression;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EpdRenderer(XteinkDisplay& display);
|
|
||||||
~EpdRenderer();
|
|
||||||
int getTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
|
||||||
int getUiTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
|
||||||
int getSmallTextWidth(const char* text, EpdFontStyle style = REGULAR) const;
|
|
||||||
void drawText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
|
||||||
void drawUiText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
|
||||||
void drawSmallText(int x, int y, const char* text, uint16_t color = 1, EpdFontStyle style = REGULAR) const;
|
|
||||||
void drawTextBox(int x, int y, const std::string& text, int width, int height, EpdFontStyle style = REGULAR) const;
|
|
||||||
void drawLine(int x1, int y1, int x2, int y2, uint16_t color = 1) const;
|
|
||||||
void drawRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
|
||||||
void fillRect(int x, int y, int width, int height, uint16_t color = 1) const;
|
|
||||||
void drawCircle(int x, int y, int radius, uint16_t color = 1) const;
|
|
||||||
void fillCircle(int x, int y, int radius, uint16_t color = 1) const;
|
|
||||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
|
|
||||||
bool mirrorY = false) const;
|
|
||||||
void drawImageNoMargin(const uint8_t bitmap[], int x, int y, int width, int height, bool invert = false,
|
|
||||||
bool mirrorY = false) const;
|
|
||||||
void clearScreen(bool black = false) const;
|
|
||||||
void flushDisplay(bool partialUpdate = true) const;
|
|
||||||
void flushArea(int x, int y, int width, int height) const;
|
|
||||||
|
|
||||||
int getPageWidth() const;
|
|
||||||
int getPageHeight() const;
|
|
||||||
int getSpaceWidth() const;
|
|
||||||
int getLineHeight() const;
|
|
||||||
// set margins
|
|
||||||
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
|
||||||
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
|
||||||
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
|
||||||
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
|
||||||
};
|
|
||||||
+120
-197
@@ -6,249 +6,166 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
#include "Epub/FsHelpers.h"
|
||||||
// open up the meta data to find where the content.opf file lives
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
size_t s;
|
#include "Epub/parsers/ContentOpfParser.h"
|
||||||
const auto metaInfo = reinterpret_cast<char*>(zip.readFileToMemory("META-INF/container.xml", &s, true));
|
#include "Epub/parsers/TocNcxParser.h"
|
||||||
if (!metaInfo) {
|
|
||||||
Serial.println("Could not find META-INF/container.xml");
|
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||||
|
const auto containerPath = "META-INF/container.xml";
|
||||||
|
size_t containerSize;
|
||||||
|
|
||||||
|
// Get file size without loading it all into heap
|
||||||
|
if (!getItemSize(containerPath, &containerSize)) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not find or size META-INF/container.xml\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the meta data
|
ContainerParser containerParser(containerSize);
|
||||||
tinyxml2::XMLDocument metaDataDoc;
|
|
||||||
const auto result = metaDataDoc.Parse(metaInfo);
|
|
||||||
free(metaInfo);
|
|
||||||
|
|
||||||
if (result != tinyxml2::XML_SUCCESS) {
|
if (!containerParser.setup()) {
|
||||||
Serial.printf("Could not parse META-INF/container.xml. Error: %d\n", result);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto container = metaDataDoc.FirstChildElement("container");
|
// Stream read (reusing your existing stream logic)
|
||||||
if (!container) {
|
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||||
Serial.println("Could not find container element in META-INF/container.xml");
|
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
||||||
|
containerParser.teardown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto rootfiles = container->FirstChildElement("rootfiles");
|
// Extract the result
|
||||||
if (!rootfiles) {
|
if (containerParser.fullPath.empty()) {
|
||||||
Serial.println("Could not find rootfiles element in META-INF/container.xml");
|
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
||||||
|
containerParser.teardown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the root file that has the media-type="application/oebps-package+xml"
|
*contentOpfFile = std::move(containerParser.fullPath);
|
||||||
auto rootfile = rootfiles->FirstChildElement("rootfile");
|
|
||||||
while (rootfile) {
|
|
||||||
const char* mediaType = rootfile->Attribute("media-type");
|
|
||||||
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
|
||||||
const char* full_path = rootfile->Attribute("full-path");
|
|
||||||
if (full_path) {
|
|
||||||
contentOpfFile = full_path;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rootfile = rootfile->NextSiblingElement("rootfile");
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.println("Could not get path to content.opf file");
|
containerParser.teardown();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
|
||||||
// read in the content.opf file and parse it
|
|
||||||
auto contents = reinterpret_cast<char*>(zip.readFileToMemory(content_opf_file.c_str(), nullptr, true));
|
|
||||||
|
|
||||||
// parse the contents
|
|
||||||
tinyxml2::XMLDocument doc;
|
|
||||||
auto result = doc.Parse(contents);
|
|
||||||
free(contents);
|
|
||||||
|
|
||||||
if (result != tinyxml2::XML_SUCCESS) {
|
|
||||||
Serial.printf("Error parsing content.opf - %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto package = doc.FirstChildElement("package");
|
|
||||||
if (!package) package = doc.FirstChildElement("opf:package");
|
|
||||||
|
|
||||||
if (!package) {
|
|
||||||
Serial.println("Could not find package element in content.opf");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the metadata - title and cover image
|
|
||||||
auto metadata = package->FirstChildElement("metadata");
|
|
||||||
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
|
|
||||||
if (!metadata) {
|
|
||||||
Serial.println("Missing metadata");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto titleEl = metadata->FirstChildElement("dc:title");
|
|
||||||
if (!titleEl) {
|
|
||||||
Serial.println("Missing title");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this->title = titleEl->GetText();
|
|
||||||
|
|
||||||
auto cover = metadata->FirstChildElement("meta");
|
|
||||||
if (!cover) cover = metadata->FirstChildElement("opf:meta");
|
|
||||||
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
|
|
||||||
cover = cover->NextSiblingElement("meta");
|
|
||||||
}
|
|
||||||
if (!cover) {
|
|
||||||
Serial.println("Missing cover");
|
|
||||||
}
|
|
||||||
auto coverItem = cover ? cover->Attribute("content") : nullptr;
|
|
||||||
|
|
||||||
// read the manifest and spine
|
|
||||||
// the manifest gives us the names of the files
|
|
||||||
// the spine gives us the order of the files
|
|
||||||
// we can then read the files in the order they are in the spine
|
|
||||||
auto manifest = package->FirstChildElement("manifest");
|
|
||||||
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
|
|
||||||
if (!manifest) {
|
|
||||||
Serial.println("Missing manifest");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a mapping from id to file name
|
|
||||||
auto item = manifest->FirstChildElement("item");
|
|
||||||
if (!item) item = manifest->FirstChildElement("opf:item");
|
|
||||||
std::map<std::string, std::string> items;
|
|
||||||
|
|
||||||
while (item) {
|
|
||||||
std::string itemId = item->Attribute("id");
|
|
||||||
std::string href = contentBasePath + item->Attribute("href");
|
|
||||||
|
|
||||||
// grab the cover image
|
|
||||||
if (coverItem && itemId == coverItem) {
|
|
||||||
coverImageItem = href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// grab the ncx file
|
|
||||||
if (itemId == "ncx" || itemId == "ncxtoc") {
|
|
||||||
tocNcxItem = href;
|
|
||||||
}
|
|
||||||
|
|
||||||
items[itemId] = href;
|
|
||||||
auto nextItem = item->NextSiblingElement("item");
|
|
||||||
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
|
|
||||||
item = nextItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the spine
|
|
||||||
auto spineEl = package->FirstChildElement("spine");
|
|
||||||
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
|
|
||||||
if (!spineEl) {
|
|
||||||
Serial.println("Missing spine");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the spine
|
|
||||||
auto itemref = spineEl->FirstChildElement("itemref");
|
|
||||||
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
|
|
||||||
while (itemref) {
|
|
||||||
auto id = itemref->Attribute("idref");
|
|
||||||
if (items.find(id) != items.end()) {
|
|
||||||
spine.emplace_back(id, items[id]);
|
|
||||||
}
|
|
||||||
auto nextItemRef = itemref->NextSiblingElement("itemref");
|
|
||||||
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
|
|
||||||
itemref = nextItemRef;
|
|
||||||
}
|
|
||||||
return true;
|
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
|
// the ncx file should have been specified in the content.opf file
|
||||||
if (tocNcxItem.empty()) {
|
if (tocNcxItem.empty()) {
|
||||||
Serial.println("No ncx file specified");
|
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxData = reinterpret_cast<char*>(zip.readFileToMemory(tocNcxItem.c_str(), nullptr, true));
|
size_t tocSize;
|
||||||
if (!ncxData) {
|
if (!getItemSize(tocNcxItem, &tocSize)) {
|
||||||
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
|
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the Toc contents
|
TocNcxParser ncxParser(contentBasePath, tocSize);
|
||||||
tinyxml2::XMLDocument doc;
|
|
||||||
const auto result = doc.Parse(ncxData);
|
|
||||||
free(ncxData);
|
|
||||||
|
|
||||||
if (result != tinyxml2::XML_SUCCESS) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("Error parsing toc %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncx = doc.FirstChildElement("ncx");
|
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||||
if (!ncx) {
|
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
|
||||||
Serial.println("Could not find first child ncx in toc");
|
ncxParser.teardown();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto navMap = ncx->FirstChildElement("navMap");
|
this->toc = std::move(ncxParser.toc);
|
||||||
if (!navMap) {
|
|
||||||
Serial.println("Could not find navMap child in ncx");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
|
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
|
||||||
|
|
||||||
|
ncxParser.teardown();
|
||||||
return true;
|
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
|
// load in the meta data for the epub file
|
||||||
bool Epub::load() {
|
bool Epub::load() {
|
||||||
|
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||||
ZipFile zip("/sd" + filepath);
|
ZipFile zip("/sd" + filepath);
|
||||||
|
|
||||||
std::string contentOpfFile;
|
std::string contentOpfFilePath;
|
||||||
if (!findContentOpfFile(zip, contentOpfFile)) {
|
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||||
Serial.println("Could not open ePub");
|
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parseTocNcxFile(zip)) {
|
if (!parseTocNcxFile()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
bool Epub::clearCache() const {
|
||||||
|
if (!SD.exists(cachePath.c_str())) {
|
||||||
|
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::removeDir(cachePath.c_str())) {
|
||||||
|
Serial.printf("[%lu] [EPB] Failed to clear cache\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [EPB] Cache cleared successfully\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void Epub::setupCacheDir() const {
|
void Epub::setupCacheDir() const {
|
||||||
if (SD.exists(cachePath.c_str())) {
|
if (SD.exists(cachePath.c_str())) {
|
||||||
@@ -314,7 +231,7 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
|
|||||||
|
|
||||||
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
|
const auto content = zip.readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
Serial.printf("Failed to read item %s\n", path.c_str());
|
Serial.printf("[%lu] [EBP] Failed to read item %s\n", millis(), path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,11 +245,18 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
|
|||||||
return zip.readFileToStream(path.c_str(), out, chunkSize);
|
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(); }
|
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||||
|
|
||||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||||
Serial.printf("getSpineItem index:%d is out of range\n", spineIndex);
|
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||||
return spine.at(0).second;
|
return spine.at(0).second;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +265,7 @@ std::string& Epub::getSpineItem(const int spineIndex) {
|
|||||||
|
|
||||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||||
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
||||||
Serial.printf("getTocItem index:%d is out of range\n", tocTndex);
|
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
|
||||||
return toc.at(0);
|
return toc.at(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +284,7 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("Section not found");
|
Serial.printf("[%lu] [EBP] Section not found\n", millis());
|
||||||
// not found - default to the start of the book
|
// not found - default to the start of the book
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -374,7 +298,6 @@ int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("TOC item not found");
|
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
|
||||||
// not found - default to first item
|
return -1;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-18
@@ -1,22 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <HardwareSerial.h>
|
#include <Print.h>
|
||||||
#include <tinyxml2.h>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class ZipFile;
|
#include "Epub/EpubTocEntry.h"
|
||||||
|
|
||||||
class EpubTocEntry {
|
class ZipFile;
|
||||||
public:
|
|
||||||
std::string title;
|
|
||||||
std::string href;
|
|
||||||
std::string anchor;
|
|
||||||
int level;
|
|
||||||
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
|
|
||||||
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class Epub {
|
class Epub {
|
||||||
// the title read from the EPUB meta data
|
// the title read from the EPUB meta data
|
||||||
@@ -36,11 +27,9 @@ class Epub {
|
|||||||
// Uniq cache key based on filepath
|
// Uniq cache key based on filepath
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
|
||||||
// find the path for the content.opf file
|
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||||
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
|
bool parseContentOpf(const std::string& contentOpfFilePath);
|
||||||
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
|
bool parseTocNcxFile();
|
||||||
bool parseTocNcxFile(const ZipFile& zip);
|
|
||||||
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
@@ -50,7 +39,7 @@ class Epub {
|
|||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
bool load();
|
bool load();
|
||||||
void clearCache() const;
|
bool clearCache() const;
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
const std::string& getPath() const;
|
const std::string& getPath() const;
|
||||||
@@ -59,6 +48,7 @@ class Epub {
|
|||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) 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);
|
std::string& getSpineItem(int spineIndex);
|
||||||
int getSpineItemsCount() const;
|
int getSpineItemsCount() const;
|
||||||
EpubTocEntry& getTocItem(int tocTndex);
|
EpubTocEntry& getTocItem(int tocTndex);
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <expat.h>
|
|
||||||
#include <limits.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "blocks/TextBlock.h"
|
|
||||||
|
|
||||||
class Page;
|
|
||||||
class EpdRenderer;
|
|
||||||
|
|
||||||
#define PART_WORD_BUFFER_SIZE 200
|
|
||||||
|
|
||||||
class EpubHtmlParserSlim {
|
|
||||||
const char* filepath;
|
|
||||||
EpdRenderer& renderer;
|
|
||||||
std::function<void(Page*)> completePageFn;
|
|
||||||
int depth = 0;
|
|
||||||
int skipUntilDepth = INT_MAX;
|
|
||||||
int boldUntilDepth = INT_MAX;
|
|
||||||
int italicUntilDepth = INT_MAX;
|
|
||||||
// If we encounter words longer than this, but this is pretty large
|
|
||||||
char partWordBuffer[PART_WORD_BUFFER_SIZE] = {};
|
|
||||||
int partWordBufferIndex = 0;
|
|
||||||
TextBlock* currentTextBlock = nullptr;
|
|
||||||
Page* currentPage = nullptr;
|
|
||||||
|
|
||||||
void startNewTextBlock(BLOCK_STYLE style);
|
|
||||||
void makePages();
|
|
||||||
// XML callbacks
|
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
|
||||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
|
||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EpubHtmlParserSlim(const char* filepath, EpdRenderer& renderer,
|
|
||||||
const std::function<void(Page*)>& completePageFn)
|
|
||||||
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
|
|
||||||
~EpubHtmlParserSlim() = default;
|
|
||||||
bool parseAndBuildPages();
|
|
||||||
};
|
|
||||||
@@ -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) {}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class FsHelpers {
|
||||||
|
public:
|
||||||
|
static bool removeDir(const char* path);
|
||||||
|
};
|
||||||
+28
-19
@@ -3,48 +3,56 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void PageLine::render(EpdRenderer& renderer) { block->render(renderer, 0, yPos); }
|
constexpr uint8_t PAGE_FILE_VERSION = 3;
|
||||||
|
|
||||||
|
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
|
||||||
|
|
||||||
void PageLine::serialize(std::ostream& os) {
|
void PageLine::serialize(std::ostream& os) {
|
||||||
|
serialization::writePod(os, xPos);
|
||||||
serialization::writePod(os, yPos);
|
serialization::writePod(os, yPos);
|
||||||
|
|
||||||
// serialize TextBlock pointed to by PageLine
|
// serialize TextBlock pointed to by PageLine
|
||||||
block->serialize(os);
|
block->serialize(os);
|
||||||
}
|
}
|
||||||
|
|
||||||
PageLine* PageLine::deserialize(std::istream& is) {
|
std::unique_ptr<PageLine> PageLine::deserialize(std::istream& is) {
|
||||||
int32_t yPos;
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
serialization::readPod(is, xPos);
|
||||||
serialization::readPod(is, yPos);
|
serialization::readPod(is, yPos);
|
||||||
|
|
||||||
const auto tb = TextBlock::deserialize(is);
|
auto tb = TextBlock::deserialize(is);
|
||||||
return new PageLine(tb, yPos);
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::render(EpdRenderer& renderer) const {
|
void Page::render(GfxRenderer& renderer, const int fontId) const {
|
||||||
const auto start = millis();
|
for (auto& element : elements) {
|
||||||
for (const auto element : elements) {
|
element->render(renderer, fontId);
|
||||||
element->render(renderer);
|
|
||||||
}
|
}
|
||||||
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Page::serialize(std::ostream& os) const {
|
void Page::serialize(std::ostream& os) const {
|
||||||
serialization::writePod(os, nextY);
|
serialization::writePod(os, PAGE_FILE_VERSION);
|
||||||
|
|
||||||
const uint32_t count = elements.size();
|
const uint32_t count = elements.size();
|
||||||
serialization::writePod(os, count);
|
serialization::writePod(os, count);
|
||||||
|
|
||||||
for (auto* el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Only PageLine exists currently
|
||||||
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
||||||
static_cast<PageLine*>(el)->serialize(os);
|
el->serialize(os);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Page* Page::deserialize(std::istream& is) {
|
std::unique_ptr<Page> Page::deserialize(std::istream& is) {
|
||||||
auto* page = new Page();
|
uint8_t version;
|
||||||
|
serialization::readPod(is, version);
|
||||||
|
if (version != PAGE_FILE_VERSION) {
|
||||||
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
serialization::readPod(is, page->nextY);
|
auto page = std::unique_ptr<Page>(new Page());
|
||||||
|
|
||||||
uint32_t count;
|
uint32_t count;
|
||||||
serialization::readPod(is, count);
|
serialization::readPod(is, count);
|
||||||
@@ -54,10 +62,11 @@ Page* Page::deserialize(std::istream& is) {
|
|||||||
serialization::readPod(is, tag);
|
serialization::readPod(is, tag);
|
||||||
|
|
||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto* pl = PageLine::deserialize(is);
|
auto pl = PageLine::deserialize(is);
|
||||||
page->elements.push_back(pl);
|
page->elements.push_back(std::move(pl));
|
||||||
} else {
|
} else {
|
||||||
throw std::runtime_error("Unknown PageElement tag");
|
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-17
@@ -1,4 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "blocks/TextBlock.h"
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
enum PageElementTag : uint8_t {
|
enum PageElementTag : uint8_t {
|
||||||
@@ -8,36 +11,31 @@ enum PageElementTag : uint8_t {
|
|||||||
// represents something that has been added to a page
|
// represents something that has been added to a page
|
||||||
class PageElement {
|
class PageElement {
|
||||||
public:
|
public:
|
||||||
int yPos;
|
int16_t xPos;
|
||||||
explicit PageElement(const int yPos) : yPos(yPos) {}
|
int16_t yPos;
|
||||||
|
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(EpdRenderer& renderer) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId) = 0;
|
||||||
virtual void serialize(std::ostream& os) = 0;
|
virtual void serialize(std::ostream& os) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
class PageLine final : public PageElement {
|
class PageLine final : public PageElement {
|
||||||
const TextBlock* block;
|
std::shared_ptr<TextBlock> block;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
|
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
~PageLine() override { delete block; }
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(EpdRenderer& renderer) override;
|
void render(GfxRenderer& renderer, int fontId) override;
|
||||||
void serialize(std::ostream& os) override;
|
void serialize(std::ostream& os) override;
|
||||||
static PageLine* deserialize(std::istream& is);
|
static std::unique_ptr<PageLine> deserialize(std::istream& is);
|
||||||
};
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
public:
|
public:
|
||||||
int nextY = 0;
|
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
std::vector<PageElement*> elements;
|
std::vector<std::shared_ptr<PageElement>> elements;
|
||||||
void render(EpdRenderer& renderer) const;
|
void render(GfxRenderer& renderer, int fontId) const;
|
||||||
~Page() {
|
|
||||||
for (const auto element : elements) {
|
|
||||||
delete element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void serialize(std::ostream& os) const;
|
void serialize(std::ostream& os) const;
|
||||||
static Page* deserialize(std::istream& is);
|
static std::unique_ptr<Page> deserialize(std::istream& is);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
+81
-42
@@ -1,37 +1,44 @@
|
|||||||
#include "Section.h"
|
#include "Section.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "EpubHtmlParserSlim.h"
|
#include "FsHelpers.h"
|
||||||
#include "Page.h"
|
#include "Page.h"
|
||||||
#include "Serialization.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 2;
|
constexpr uint8_t SECTION_FILE_VERSION = 4;
|
||||||
|
|
||||||
void Section::onPageComplete(const Page* page) {
|
|
||||||
Serial.printf("Page %d complete - free mem: %lu\n", pageCount, ESP.getFreeHeap());
|
|
||||||
|
|
||||||
|
void Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||||
|
|
||||||
std::ofstream outputFile("/sd" + filePath);
|
std::ofstream outputFile("/sd" + filePath);
|
||||||
page->serialize(outputFile);
|
page->serialize(outputFile);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||||
|
|
||||||
pageCount++;
|
pageCount++;
|
||||||
delete page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::writeCacheMetadata() const {
|
void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||||
|
const int marginRight, const int marginBottom, const int marginLeft) const {
|
||||||
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
std::ofstream outputFile(("/sd" + cachePath + "/section.bin").c_str());
|
||||||
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
serialization::writePod(outputFile, SECTION_FILE_VERSION);
|
||||||
|
serialization::writePod(outputFile, fontId);
|
||||||
|
serialization::writePod(outputFile, lineCompression);
|
||||||
|
serialization::writePod(outputFile, marginTop);
|
||||||
|
serialization::writePod(outputFile, marginRight);
|
||||||
|
serialization::writePod(outputFile, marginBottom);
|
||||||
|
serialization::writePod(outputFile, marginLeft);
|
||||||
serialization::writePod(outputFile, pageCount);
|
serialization::writePod(outputFile, pageCount);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Section::loadCacheMetadata() {
|
bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop,
|
||||||
|
const int marginRight, const int marginBottom, const int marginLeft) {
|
||||||
if (!SD.exists(cachePath.c_str())) {
|
if (!SD.exists(cachePath.c_str())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -42,17 +49,39 @@ bool Section::loadCacheMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
|
std::ifstream inputFile(("/sd" + sectionFilePath).c_str());
|
||||||
uint8_t version;
|
|
||||||
serialization::readPod(inputFile, version);
|
// Match parameters
|
||||||
if (version != SECTION_FILE_VERSION) {
|
{
|
||||||
inputFile.close();
|
uint8_t version;
|
||||||
SD.remove(sectionFilePath.c_str());
|
serialization::readPod(inputFile, version);
|
||||||
Serial.printf("Section state file: Unknown version %u\n", version);
|
if (version != SECTION_FILE_VERSION) {
|
||||||
return false;
|
inputFile.close();
|
||||||
|
Serial.printf("[%lu] [SCT] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
|
clearCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft;
|
||||||
|
float fileLineCompression;
|
||||||
|
serialization::readPod(inputFile, fileFontId);
|
||||||
|
serialization::readPod(inputFile, fileLineCompression);
|
||||||
|
serialization::readPod(inputFile, fileMarginTop);
|
||||||
|
serialization::readPod(inputFile, fileMarginRight);
|
||||||
|
serialization::readPod(inputFile, fileMarginBottom);
|
||||||
|
serialization::readPod(inputFile, fileMarginLeft);
|
||||||
|
|
||||||
|
if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop ||
|
||||||
|
marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft) {
|
||||||
|
inputFile.close();
|
||||||
|
Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis());
|
||||||
|
clearCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serialization::readPod(inputFile, pageCount);
|
serialization::readPod(inputFile, pageCount);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
Serial.printf("Loaded cache: %d pages\n", pageCount);
|
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +90,24 @@ void Section::setupCacheDir() const {
|
|||||||
SD.mkdir(cachePath.c_str());
|
SD.mkdir(cachePath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||||
|
bool Section::clearCache() const {
|
||||||
|
if (!SD.exists(cachePath.c_str())) {
|
||||||
|
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool Section::persistPageDataToSD() {
|
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) {
|
||||||
const auto localPath = epub->getSpineItem(spineIndex);
|
const auto localPath = epub->getSpineItem(spineIndex);
|
||||||
|
|
||||||
// TODO: Should we get rid of this file all together?
|
// TODO: Should we get rid of this file all together?
|
||||||
@@ -75,44 +119,39 @@ bool Section::persistPageDataToSD() {
|
|||||||
f.close();
|
f.close();
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.println("Failed to stream item contents");
|
Serial.printf("[%lu] [SCT] Failed to stream item contents to temp file\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Streamed HTML to %s\n", tmpHtmlPath.c_str());
|
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s\n", millis(), tmpHtmlPath.c_str());
|
||||||
|
|
||||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
||||||
|
|
||||||
auto visitor =
|
ChapterHtmlSlimParser visitor(sdTmpHtmlPath.c_str(), renderer, fontId, lineCompression, marginTop, marginRight,
|
||||||
EpubHtmlParserSlim(sdTmpHtmlPath.c_str(), renderer, [this](const Page* page) { this->onPageComplete(page); });
|
marginBottom, marginLeft,
|
||||||
|
[this](std::unique_ptr<Page> page) { this->onPageComplete(std::move(page)); });
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
SD.remove(tmpHtmlPath.c_str());
|
SD.remove(tmpHtmlPath.c_str());
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.println("Failed to parse and build pages");
|
Serial.printf("[%lu] [SCT] Failed to parse XML and build pages\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeCacheMetadata();
|
writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Section::renderPage() const {
|
std::unique_ptr<Page> Section::loadPageFromSD() const {
|
||||||
if (0 <= currentPage && currentPage < pageCount) {
|
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
||||||
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
if (!SD.exists(filePath.c_str() + 3)) {
|
||||||
std::ifstream inputFile(filePath);
|
Serial.printf("[%lu] [SCT] Page file does not exist: %s\n", millis(), filePath.c_str());
|
||||||
const Page* p = Page::deserialize(inputFile);
|
return nullptr;
|
||||||
inputFile.close();
|
|
||||||
p->render(renderer);
|
|
||||||
delete p;
|
|
||||||
} else if (pageCount == 0) {
|
|
||||||
Serial.println("No pages to render");
|
|
||||||
const int width = renderer.getTextWidth("Empty chapter", BOLD);
|
|
||||||
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Empty chapter", 1, BOLD);
|
|
||||||
} else {
|
|
||||||
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
|
|
||||||
const int width = renderer.getTextWidth("Out of bounds", BOLD);
|
|
||||||
renderer.drawText((renderer.getPageWidth() - width) / 2, 300, "Out of bounds", 1, BOLD);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::ifstream inputFile(filePath);
|
||||||
|
auto page = Page::deserialize(inputFile);
|
||||||
|
inputFile.close();
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-10
@@ -1,30 +1,35 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class EpdRenderer;
|
class GfxRenderer;
|
||||||
|
|
||||||
class Section {
|
class Section {
|
||||||
Epub* epub;
|
std::shared_ptr<Epub> epub;
|
||||||
const int spineIndex;
|
const int spineIndex;
|
||||||
EpdRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
|
||||||
void onPageComplete(const Page* page);
|
void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||||
|
int marginLeft) const;
|
||||||
|
void onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int pageCount = 0;
|
int pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(Epub* epub, const int spineIndex, EpdRenderer& renderer)
|
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
||||||
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
||||||
}
|
}
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
void writeCacheMetadata() const;
|
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||||
bool loadCacheMetadata();
|
int marginLeft);
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
void clearCache() const;
|
bool clearCache() const;
|
||||||
bool persistPageDataToSD();
|
bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||||
void renderPage() const;
|
int marginLeft);
|
||||||
|
std::unique_ptr<Page> loadPageFromSD() const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
class EpdRenderer;
|
class GfxRenderer;
|
||||||
|
|
||||||
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
|||||||
class Block {
|
class Block {
|
||||||
public:
|
public:
|
||||||
virtual ~Block() = default;
|
virtual ~Block() = default;
|
||||||
virtual void layout(EpdRenderer& renderer) = 0;
|
virtual void layout(GfxRenderer& renderer) = 0;
|
||||||
virtual BlockType getType() = 0;
|
virtual BlockType getType() = 0;
|
||||||
virtual bool isEmpty() = 0;
|
virtual bool isEmpty() = 0;
|
||||||
virtual void finish() {}
|
virtual void finish() {}
|
||||||
|
|||||||
@@ -1,171 +1,19 @@
|
|||||||
#include "TextBlock.h"
|
#include "TextBlock.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
void TextBlock::addWord(const std::string& word, const bool is_bold, const bool is_italic) {
|
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||||
if (word.length() == 0) return;
|
auto wordIt = words.begin();
|
||||||
|
auto wordStylesIt = wordStyles.begin();
|
||||||
|
auto wordXposIt = wordXpos.begin();
|
||||||
|
|
||||||
words.push_back(word);
|
|
||||||
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer& renderer) {
|
|
||||||
const int totalWordCount = words.size();
|
|
||||||
const int pageWidth = renderer.getPageWidth();
|
|
||||||
const int spaceWidth = renderer.getSpaceWidth();
|
|
||||||
|
|
||||||
words.shrink_to_fit();
|
|
||||||
wordStyles.shrink_to_fit();
|
|
||||||
wordXpos.reserve(totalWordCount);
|
|
||||||
|
|
||||||
// measure each word
|
|
||||||
uint16_t wordWidths[totalWordCount];
|
|
||||||
for (int i = 0; i < words.size(); i++) {
|
for (int i = 0; i < words.size(); i++) {
|
||||||
// measure the word
|
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||||
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(words[i].c_str(), fontStyle);
|
|
||||||
wordWidths[i] = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
// now apply the dynamic programming algorithm to find the best line breaks
|
std::advance(wordIt, 1);
|
||||||
// DP table in which dp[i] represents cost of line starting with word words[i]
|
std::advance(wordStylesIt, 1);
|
||||||
int dp[totalWordCount];
|
std::advance(wordXposIt, 1);
|
||||||
|
|
||||||
// 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 EpdRenderer& renderer, const int x, const int y) const {
|
|
||||||
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(x + wordXpos[i], y, words[i].c_str(), 1, fontStyle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +37,11 @@ void TextBlock::serialize(std::ostream& os) const {
|
|||||||
serialization::writePod(os, style);
|
serialization::writePod(os, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextBlock* TextBlock::deserialize(std::istream& is) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(std::istream& is) {
|
||||||
uint32_t wc, xc, sc;
|
uint32_t wc, xc, sc;
|
||||||
std::vector<std::string> words;
|
std::list<std::string> words;
|
||||||
std::vector<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::vector<uint8_t> wordStyles;
|
std::list<EpdFontStyle> wordStyles;
|
||||||
BLOCK_STYLE style;
|
BLOCK_STYLE style;
|
||||||
|
|
||||||
// words
|
// words
|
||||||
@@ -214,5 +62,5 @@ TextBlock* TextBlock::deserialize(std::istream& is) {
|
|||||||
// style
|
// style
|
||||||
serialization::readPod(is, style);
|
serialization::readPod(is, style);
|
||||||
|
|
||||||
return new TextBlock(words, wordXpos, wordStyles, style);
|
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,40 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Block.h"
|
#include "Block.h"
|
||||||
|
|
||||||
enum SPAN_STYLE : uint8_t {
|
|
||||||
BOLD_SPAN = 1,
|
|
||||||
ITALIC_SPAN = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum BLOCK_STYLE : uint8_t {
|
|
||||||
JUSTIFIED = 0,
|
|
||||||
LEFT_ALIGN = 1,
|
|
||||||
CENTER_ALIGN = 2,
|
|
||||||
RIGHT_ALIGN = 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
// represents a block of words in the html document
|
// represents a block of words in the html document
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
// pointer to each word
|
public:
|
||||||
std::vector<std::string> words;
|
enum BLOCK_STYLE : uint8_t {
|
||||||
// x position of each word
|
JUSTIFIED = 0,
|
||||||
std::vector<uint16_t> wordXpos;
|
LEFT_ALIGN = 1,
|
||||||
// the styles of each word
|
CENTER_ALIGN = 2,
|
||||||
std::vector<uint8_t> wordStyles;
|
RIGHT_ALIGN = 3,
|
||||||
|
};
|
||||||
|
|
||||||
// the style of the block - left, center, right aligned
|
private:
|
||||||
|
std::list<std::string> words;
|
||||||
|
std::list<uint16_t> wordXpos;
|
||||||
|
std::list<EpdFontStyle> wordStyles;
|
||||||
BLOCK_STYLE style;
|
BLOCK_STYLE style;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(const BLOCK_STYLE style) : style(style) {}
|
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
|
||||||
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos,
|
const BLOCK_STYLE style)
|
||||||
// the styles of each word
|
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
||||||
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
|
|
||||||
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
|
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
void addWord(const std::string& word, bool is_bold, bool is_italic);
|
|
||||||
void setStyle(const BLOCK_STYLE style) { this->style = style; }
|
void setStyle(const BLOCK_STYLE style) { this->style = style; }
|
||||||
BLOCK_STYLE getStyle() const { return style; }
|
BLOCK_STYLE getStyle() const { return style; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(EpdRenderer& renderer) override {};
|
void layout(GfxRenderer& renderer) override {};
|
||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
std::list<TextBlock*> splitIntoLines(const EpdRenderer& renderer);
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
void render(const EpdRenderer& renderer, int x, int y) const;
|
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
void serialize(std::ostream& os) const;
|
void serialize(std::ostream& os) const;
|
||||||
static TextBlock* deserialize(std::istream& is);
|
static std::unique_ptr<TextBlock> deserialize(std::istream& is);
|
||||||
};
|
};
|
||||||
|
|||||||
+71
-85
@@ -1,11 +1,11 @@
|
|||||||
#include "EpubHtmlParserSlim.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
#include "Page.h"
|
#include "../Page.h"
|
||||||
#include "htmlEntities.h"
|
#include "../htmlEntities.h"
|
||||||
|
|
||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
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
|
// start a new text block if needed
|
||||||
void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
|
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
// already have a text block running and it is empty - just reuse it
|
// already have a text block running and it is empty - just reuse it
|
||||||
if (currentTextBlock->isEmpty()) {
|
if (currentTextBlock->isEmpty()) {
|
||||||
@@ -46,15 +46,13 @@ void EpubHtmlParserSlim::startNewTextBlock(const BLOCK_STYLE style) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTextBlock->finish();
|
|
||||||
makePages();
|
makePages();
|
||||||
delete currentTextBlock;
|
|
||||||
}
|
}
|
||||||
currentTextBlock = new TextBlock(style);
|
currentTextBlock.reset(new ParsedText(style));
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
(void)atts;
|
(void)atts;
|
||||||
|
|
||||||
// Middle of skip
|
// 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)) {
|
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||||
// const char* src = element.Attribute("src");
|
// TODO: Start processing image tags
|
||||||
// if (src) {
|
|
||||||
// // don't leave an empty text block in the list
|
|
||||||
// // const BLOCK_STYLE style = currentTextBlock->get_style();
|
|
||||||
// if (currentTextBlock->isEmpty()) {
|
|
||||||
// delete currentTextBlock;
|
|
||||||
// currentTextBlock = nullptr;
|
|
||||||
// }
|
|
||||||
// // TODO: Fix this
|
|
||||||
// // blocks.push_back(new ImageBlock(m_base_path + src));
|
|
||||||
// // start a new text block - with the same style as before
|
|
||||||
// // startNewTextBlock(style);
|
|
||||||
// } else {
|
|
||||||
// // ESP_LOGE(TAG, "Could not find src attribute");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// start skip
|
|
||||||
self->skipUntilDepth = self->depth;
|
self->skipUntilDepth = self->depth;
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
return;
|
return;
|
||||||
@@ -94,13 +76,13 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
self->startNewTextBlock(CENTER_ALIGN);
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||||
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
||||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
if (strcmp(name, "br") == 0) {
|
if (strcmp(name, "br") == 0) {
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||||
} else {
|
} else {
|
||||||
self->startNewTextBlock(JUSTIFIED);
|
self->startNewTextBlock(TextBlock::JUSTIFIED);
|
||||||
}
|
}
|
||||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
||||||
@@ -111,21 +93,29 @@ void XMLCALL EpubHtmlParserSlim::startElement(void* userData, const XML_Char* na
|
|||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s, const int len) {
|
void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
|
|
||||||
// Middle of skip
|
// Middle of skip
|
||||||
if (self->skipUntilDepth < self->depth) {
|
if (self->skipUntilDepth < self->depth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EpdFontStyle fontStyle = REGULAR;
|
||||||
|
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||||
|
fontStyle = BOLD_ITALIC;
|
||||||
|
} else if (self->boldUntilDepth < self->depth) {
|
||||||
|
fontStyle = BOLD;
|
||||||
|
} else if (self->italicUntilDepth < self->depth) {
|
||||||
|
fontStyle = ITALIC;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
if (isWhitespace(s[i])) {
|
if (isWhitespace(s[i])) {
|
||||||
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||||
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
|
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
|
||||||
self->italicUntilDepth < self->depth);
|
|
||||||
self->partWordBufferIndex = 0;
|
self->partWordBufferIndex = 0;
|
||||||
}
|
}
|
||||||
// Skip the whitespace char
|
// Skip the whitespace char
|
||||||
@@ -133,10 +123,9 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we're about to run out of space, then cut the word off and start a new one
|
// If we're about to run out of space, then cut the word off and start a new one
|
||||||
if (self->partWordBufferIndex >= PART_WORD_BUFFER_SIZE - 2) {
|
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||||
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
|
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
|
||||||
self->italicUntilDepth < self->depth);
|
|
||||||
self->partWordBufferIndex = 0;
|
self->partWordBufferIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +133,8 @@ void XMLCALL EpubHtmlParserSlim::characterData(void* userData, const XML_Char* s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<EpubHtmlParserSlim*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
(void)name;
|
(void)name;
|
||||||
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
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;
|
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldBreakText) {
|
if (shouldBreakText) {
|
||||||
|
EpdFontStyle fontStyle = REGULAR;
|
||||||
|
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||||
|
fontStyle = BOLD_ITALIC;
|
||||||
|
} else if (self->boldUntilDepth < self->depth) {
|
||||||
|
fontStyle = BOLD;
|
||||||
|
} else if (self->italicUntilDepth < self->depth) {
|
||||||
|
fontStyle = ITALIC;
|
||||||
|
}
|
||||||
|
|
||||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||||
self->currentTextBlock->addWord(replaceHtmlEntities(self->partWordBuffer), self->boldUntilDepth < self->depth,
|
self->currentTextBlock->addWord(std::move(replaceHtmlEntities(self->partWordBuffer)), fontStyle);
|
||||||
self->italicUntilDepth < self->depth);
|
|
||||||
self->partWordBufferIndex = 0;
|
self->partWordBufferIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,14 +180,14 @@ void XMLCALL EpubHtmlParserSlim::endElement(void* userData, const XML_Char* name
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EpubHtmlParserSlim::parseAndBuildPages() {
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||||
startNewTextBlock(JUSTIFIED);
|
startNewTextBlock(TextBlock::JUSTIFIED);
|
||||||
|
|
||||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||||
int done;
|
int done;
|
||||||
|
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
Serial.println("Couldn't allocate memory for parser");
|
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +197,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
|||||||
|
|
||||||
FILE* file = fopen(filepath, "r");
|
FILE* file = fopen(filepath, "r");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Serial.printf("Couldn't open file %s\n", filepath);
|
Serial.printf("[%lu] [EHP] Couldn't open file %s\n", millis(), filepath);
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -208,7 +205,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
|||||||
do {
|
do {
|
||||||
void* const buf = XML_GetBuffer(parser, 1024);
|
void* const buf = XML_GetBuffer(parser, 1024);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
Serial.println("Couldn't allocate memory for buffer");
|
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@@ -217,7 +214,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
|||||||
const size_t len = fread(buf, 1, 1024, file);
|
const size_t len = fread(buf, 1, 1024, file);
|
||||||
|
|
||||||
if (ferror(file)) {
|
if (ferror(file)) {
|
||||||
Serial.println("Read error");
|
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@@ -226,7 +223,7 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
|||||||
done = feof(file);
|
done = feof(file);
|
||||||
|
|
||||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||||
Serial.printf("Parse error at line %lu:\n%s\n", XML_GetCurrentLineNumber(parser),
|
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
@@ -240,54 +237,43 @@ bool EpubHtmlParserSlim::parseAndBuildPages() {
|
|||||||
// Process last page if there is still text
|
// Process last page if there is still text
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
makePages();
|
makePages();
|
||||||
completePageFn(currentPage);
|
completePageFn(std::move(currentPage));
|
||||||
currentPage = nullptr;
|
currentPage.reset();
|
||||||
delete currentTextBlock;
|
currentTextBlock.reset();
|
||||||
currentTextBlock = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (!currentTextBlock) {
|
||||||
Serial.println("!! No text block to make pages for !!");
|
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentPage) {
|
if (!currentPage) {
|
||||||
currentPage = new Page();
|
currentPage.reset(new Page());
|
||||||
|
currentPageNextY = marginTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight();
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
const int pageHeight = renderer.getPageHeight();
|
currentTextBlock->layoutAndExtractLines(
|
||||||
|
renderer, fontId, marginLeft + marginRight,
|
||||||
// Long running task, make sure to let other things happen
|
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
||||||
vTaskDelay(1);
|
// Extra paragrpah spacing
|
||||||
|
currentPageNextY += lineHeight / 2;
|
||||||
if (currentTextBlock->getType() == TEXT_BLOCK) {
|
|
||||||
const auto lines = currentTextBlock->splitIntoLines(renderer);
|
|
||||||
|
|
||||||
for (const auto line : lines) {
|
|
||||||
if (currentPage->nextY + lineHeight > pageHeight) {
|
|
||||||
completePageFn(currentPage);
|
|
||||||
currentPage = new Page();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
|
|
||||||
currentPage->nextY += lineHeight;
|
|
||||||
}
|
|
||||||
// add some extra line between blocks
|
|
||||||
currentPage->nextY += lineHeight / 2;
|
|
||||||
}
|
|
||||||
// TODO: Image block support
|
|
||||||
// if (block->getType() == BlockType::IMAGE_BLOCK) {
|
|
||||||
// ImageBlock *imageBlock = (ImageBlock *)block;
|
|
||||||
// if (y + imageBlock->height > page_height) {
|
|
||||||
// pages.push_back(new Page());
|
|
||||||
// y = 0;
|
|
||||||
// }
|
|
||||||
// pages.back()->elements.push_back(new PageImage(imageBlock, y));
|
|
||||||
// y += imageBlock->height;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include <climits>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "../ParsedText.h"
|
||||||
|
#include "../blocks/TextBlock.h"
|
||||||
|
|
||||||
|
class Page;
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
|
class ChapterHtmlSlimParser {
|
||||||
|
const char* filepath;
|
||||||
|
GfxRenderer& renderer;
|
||||||
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
|
int depth = 0;
|
||||||
|
int skipUntilDepth = INT_MAX;
|
||||||
|
int boldUntilDepth = INT_MAX;
|
||||||
|
int italicUntilDepth = INT_MAX;
|
||||||
|
// buffer for building up words from characters, will auto break if longer than this
|
||||||
|
// leave one char at end for null pointer
|
||||||
|
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||||
|
int partWordBufferIndex = 0;
|
||||||
|
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
|
||||||
|
std::unique_ptr<Page> currentPage = nullptr;
|
||||||
|
int16_t currentPageNextY = 0;
|
||||||
|
int fontId;
|
||||||
|
float lineCompression;
|
||||||
|
int marginTop;
|
||||||
|
int marginRight;
|
||||||
|
int marginBottom;
|
||||||
|
int marginLeft;
|
||||||
|
|
||||||
|
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
|
||||||
|
void makePages();
|
||||||
|
// XML callbacks
|
||||||
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit 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(std::unique_ptr<Page>)>& completePageFn)
|
||||||
|
: filepath(filepath),
|
||||||
|
renderer(renderer),
|
||||||
|
fontId(fontId),
|
||||||
|
lineCompression(lineCompression),
|
||||||
|
marginTop(marginTop),
|
||||||
|
marginRight(marginRight),
|
||||||
|
marginBottom(marginBottom),
|
||||||
|
marginLeft(marginLeft),
|
||||||
|
completePageFn(completePageFn) {}
|
||||||
|
~ChapterHtmlSlimParser() = default;
|
||||||
|
bool parseAndBuildPages();
|
||||||
|
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||||
|
|
||||||
|
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||||
|
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||||
|
|
||||||
|
// Early return if no framebuffer is set
|
||||||
|
if (!frameBuffer) {
|
||||||
|
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate coordinates: portrait (480x800) -> landscape (800x480)
|
||||||
|
// Rotation: 90 degrees clockwise
|
||||||
|
const int rotatedX = y;
|
||||||
|
const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
|
|
||||||
|
// Bounds checking (portrait: 480x800)
|
||||||
|
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
||||||
|
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
||||||
|
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d)\n", millis(), x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate byte position and bit position
|
||||||
|
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
||||||
|
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
||||||
|
} else {
|
||||||
|
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontStyle style) const {
|
||||||
|
if (fontMap.count(fontId) == 0) {
|
||||||
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = 0, h = 0;
|
||||||
|
fontMap.at(fontId).getTextDimensions(text, &w, &h, style);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
|
||||||
|
const EpdFontStyle style) const {
|
||||||
|
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
||||||
|
drawText(fontId, x, y, text, black, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||||
|
const EpdFontStyle style) const {
|
||||||
|
const int yPos = y + getLineHeight(fontId);
|
||||||
|
int xpos = x;
|
||||||
|
|
||||||
|
// cannot draw a NULL / empty string
|
||||||
|
if (text == nullptr || *text == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontMap.count(fontId) == 0) {
|
||||||
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto font = fontMap.at(fontId);
|
||||||
|
|
||||||
|
// no printable characters
|
||||||
|
if (!font.hasPrintableChars(text, style)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cp;
|
||||||
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
|
renderChar(font, cp, &xpos, &yPos, black, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
||||||
|
if (x1 == x2) {
|
||||||
|
if (y2 < y1) {
|
||||||
|
std::swap(y1, y2);
|
||||||
|
}
|
||||||
|
for (int y = y1; y <= y2; y++) {
|
||||||
|
drawPixel(x1, y, state);
|
||||||
|
}
|
||||||
|
} else if (y1 == y2) {
|
||||||
|
if (x2 < x1) {
|
||||||
|
std::swap(x1, x2);
|
||||||
|
}
|
||||||
|
for (int x = x1; x <= x2; x++) {
|
||||||
|
drawPixel(x, y1, state);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: Implement
|
||||||
|
Serial.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
|
drawLine(x, y, x + width - 1, y, state);
|
||||||
|
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
|
||||||
|
drawLine(x + width - 1, y + height - 1, x, y + height - 1, state);
|
||||||
|
drawLine(x, y, x, y + height - 1, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
|
drawLine(x, fillY, x + width - 1, fillY, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||||
|
// Flip X and Y for portrait mode
|
||||||
|
einkDisplay.drawImage(bitmap, y, x, height, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
||||||
|
|
||||||
|
void GfxRenderer::invertScreen() const {
|
||||||
|
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
||||||
|
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
||||||
|
buffer[i] = ~buffer[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
||||||
|
einkDisplay.displayBuffer(refreshMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Support partial window update
|
||||||
|
// void GfxRenderer::flushArea(const int x, const int y, const int width, const int height) const {
|
||||||
|
// const int rotatedX = y;
|
||||||
|
// const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||||
|
//
|
||||||
|
// einkDisplay.displayBuffer(EInkDisplay::FAST_REFRESH, rotatedX, rotatedY, height, width);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation
|
||||||
|
int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; }
|
||||||
|
int GfxRenderer::getScreenHeight() { return EInkDisplay::DISPLAY_WIDTH; }
|
||||||
|
|
||||||
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||||
|
if (fontMap.count(fontId) == 0) {
|
||||||
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontMap.at(fontId).getGlyph(' ', REGULAR)->advanceX;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||||
|
if (fontMap.count(fontId) == 0) {
|
||||||
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()); }
|
||||||
|
|
||||||
|
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
|
||||||
|
|
||||||
|
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
||||||
|
const bool pixelState, const EpdFontStyle style) const {
|
||||||
|
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||||
|
if (!glyph) {
|
||||||
|
// TODO: Replace with fallback glyph property?
|
||||||
|
glyph = fontFamily.getGlyph('?', style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// no glyph?
|
||||||
|
if (!glyph) {
|
||||||
|
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int is2Bit = fontFamily.getData(style)->is2Bit;
|
||||||
|
const uint32_t offset = glyph->dataOffset;
|
||||||
|
const uint8_t width = glyph->width;
|
||||||
|
const uint8_t height = glyph->height;
|
||||||
|
const int left = glyph->left;
|
||||||
|
|
||||||
|
const uint8_t* bitmap = nullptr;
|
||||||
|
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
||||||
|
|
||||||
|
if (bitmap != nullptr) {
|
||||||
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||||
|
const int screenY = *y - glyph->top + glyphY;
|
||||||
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
||||||
|
const int pixelPosition = glyphY * width + glyphX;
|
||||||
|
const int screenX = *x + left + glyphX;
|
||||||
|
|
||||||
|
if (is2Bit) {
|
||||||
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
||||||
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
||||||
|
|
||||||
|
const uint8_t val = (byte >> bit_index) & 0x3;
|
||||||
|
if (fontRenderMode == BW && val > 0) {
|
||||||
|
drawPixel(screenX, screenY, pixelState);
|
||||||
|
} else if (fontRenderMode == GRAYSCALE_MSB && val == 1) {
|
||||||
|
// TODO: Not sure how this anti-aliasing goes on black backgrounds
|
||||||
|
drawPixel(screenX, screenY, false);
|
||||||
|
} else if (fontRenderMode == GRAYSCALE_LSB && val == 2) {
|
||||||
|
drawPixel(screenX, screenY, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
||||||
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
||||||
|
|
||||||
|
if ((byte >> bit_index) & 1) {
|
||||||
|
drawPixel(screenX, screenY, pixelState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*x += glyph->advanceX;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EInkDisplay.h>
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
class GfxRenderer {
|
||||||
|
public:
|
||||||
|
enum FontRenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
||||||
|
|
||||||
|
private:
|
||||||
|
EInkDisplay& einkDisplay;
|
||||||
|
FontRenderMode fontRenderMode;
|
||||||
|
std::map<int, EpdFontFamily> fontMap;
|
||||||
|
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
||||||
|
EpdFontStyle style) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), fontRenderMode(BW) {}
|
||||||
|
~GfxRenderer() = default;
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
void insertFont(int fontId, EpdFontFamily font);
|
||||||
|
|
||||||
|
// Screen ops
|
||||||
|
static int getScreenWidth();
|
||||||
|
static int getScreenHeight();
|
||||||
|
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
||||||
|
void invertScreen() const;
|
||||||
|
void clearScreen(uint8_t color = 0xFF) const;
|
||||||
|
|
||||||
|
// Drawing
|
||||||
|
void drawPixel(int x, int y, bool state = true) const;
|
||||||
|
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||||
|
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
|
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||||
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||||
|
|
||||||
|
// Text
|
||||||
|
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
|
||||||
|
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||||
|
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||||
|
void setFontRenderMode(const FontRenderMode mode) { this->fontRenderMode = mode; }
|
||||||
|
int getSpaceWidth(int fontId) const;
|
||||||
|
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;
|
||||||
|
};
|
||||||
+42
-25
@@ -7,7 +7,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.println("Failed to allocate memory for inflator");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||||
@@ -20,7 +20,7 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
|||||||
free(inflator);
|
free(inflator);
|
||||||
|
|
||||||
if (status != TINFL_STATUS_DONE) {
|
if (status != TINFL_STATUS_DONE) {
|
||||||
Serial.printf("tinfl_decompress() failed with status %d\n", status);
|
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,20 +32,22 @@ bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileS
|
|||||||
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
Serial.printf("mz_zip_reader_init_file() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed! Error: %s\n", millis(),
|
||||||
|
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the file
|
// find the file
|
||||||
mz_uint32 fileIndex = 0;
|
mz_uint32 fileIndex = 0;
|
||||||
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
||||||
Serial.printf("Could not find file %s\n", filename);
|
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
|
||||||
mz_zip_reader_end(&zipArchive);
|
mz_zip_reader_end(&zipArchive);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
|
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
|
||||||
Serial.printf("mz_zip_reader_file_stat() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(),
|
||||||
|
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||||
mz_zip_reader_end(&zipArchive);
|
mz_zip_reader_end(&zipArchive);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -65,13 +67,13 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
if (read != localHeaderSize) {
|
if (read != localHeaderSize) {
|
||||||
Serial.println("Something went wrong reading the local header");
|
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||||
Serial.println("Not a valid zip file header");
|
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +82,16 @@ long ZipFile::getDataOffset(const mz_zip_archive_file_stat& fileStat) const {
|
|||||||
return fileOffset + localHeaderSize + filenameLength + extraOffset;
|
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 {
|
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) const {
|
||||||
mz_zip_archive_file_stat fileStat;
|
mz_zip_archive_file_stat fileStat;
|
||||||
if (!loadFileStat(filename, &fileStat)) {
|
if (!loadFileStat(filename, &fileStat)) {
|
||||||
@@ -105,7 +117,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
if (dataRead != inflatedDataSize) {
|
if (dataRead != inflatedDataSize) {
|
||||||
Serial.println("Failed to read data");
|
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -115,7 +127,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
// Read out deflated content from file
|
// Read out deflated content from file
|
||||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||||
if (deflatedData == nullptr) {
|
if (deflatedData == nullptr) {
|
||||||
Serial.println("Failed to allocate memory for decompression buffer");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -124,7 +136,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
if (dataRead != deflatedDataSize) {
|
if (dataRead != deflatedDataSize) {
|
||||||
Serial.printf("Failed to read data, expected %d got %d\n", deflatedDataSize, dataRead);
|
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -134,14 +146,14 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
|||||||
free(deflatedData);
|
free(deflatedData);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Serial.println("Failed to inflate file");
|
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
||||||
free(data);
|
free(data);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue out of block with data set
|
// Continue out of block with data set
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Unsupported compression method");
|
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -172,7 +184,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// no deflation, just read content
|
// no deflation, just read content
|
||||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!buffer) {
|
if (!buffer) {
|
||||||
Serial.println("Failed to allocate memory for buffer");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -181,7 +193,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
while (remaining > 0) {
|
while (remaining > 0) {
|
||||||
const size_t dataRead = fread(buffer, 1, remaining < chunkSize ? remaining : chunkSize, file);
|
const size_t dataRead = fread(buffer, 1, remaining < chunkSize ? remaining : chunkSize, file);
|
||||||
if (dataRead == 0) {
|
if (dataRead == 0) {
|
||||||
Serial.println("Could not read more bytes");
|
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
||||||
free(buffer);
|
free(buffer);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@@ -200,7 +212,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup inflator
|
// Setup inflator
|
||||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.println("Failed to allocate memory for inflator");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -210,7 +222,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Setup file read buffer
|
// Setup file read buffer
|
||||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!fileReadBuffer) {
|
if (!fileReadBuffer) {
|
||||||
Serial.println("Failed to allocate memory for zip file read buffer");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
||||||
free(inflator);
|
free(inflator);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
return false;
|
return false;
|
||||||
@@ -218,7 +230,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
|
|
||||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||||
if (!outputBuffer) {
|
if (!outputBuffer) {
|
||||||
Serial.println("Failed to allocate memory for dictionary");
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
||||||
free(inflator);
|
free(inflator);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
@@ -266,16 +278,20 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
// Write output chunk
|
// Write output chunk
|
||||||
if (outBytes > 0) {
|
if (outBytes > 0) {
|
||||||
processedOutputBytes += outBytes;
|
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)
|
// Update output position in buffer (with wraparound)
|
||||||
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
|
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Decompressing - %d/%d deflated into %d/%d inflated\n", deflatedDataSize - fileRemainingBytes,
|
|
||||||
deflatedDataSize, processedOutputBytes, inflatedDataSize);
|
|
||||||
|
|
||||||
if (status < 0) {
|
if (status < 0) {
|
||||||
Serial.printf("tinfl_decompress() failed with status %d\n", status);
|
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
free(outputBuffer);
|
free(outputBuffer);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
@@ -284,7 +300,8 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status == TINFL_STATUS_DONE) {
|
if (status == TINFL_STATUS_DONE) {
|
||||||
Serial.println("Decompression finished");
|
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
||||||
|
inflatedDataSize);
|
||||||
fclose(file);
|
fclose(file);
|
||||||
free(inflator);
|
free(inflator);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
@@ -294,7 +311,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, EOF reached without TINFL_STATUS_DONE
|
// If we get here, EOF reached without TINFL_STATUS_DONE
|
||||||
Serial.println("Unexpected EOF");
|
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
||||||
fclose(file);
|
fclose(file);
|
||||||
free(outputBuffer);
|
free(outputBuffer);
|
||||||
free(fileReadBuffer);
|
free(fileReadBuffer);
|
||||||
@@ -302,6 +319,6 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("Unsupported compression method");
|
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ZipFile {
|
|||||||
public:
|
public:
|
||||||
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
|
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
|
||||||
~ZipFile() = default;
|
~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;
|
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;
|
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
Submodule open-x4-sdk updated: 8224d278c5...7e0dce9167
+26
-20
@@ -1,14 +1,8 @@
|
|||||||
; PlatformIO Project Configuration File
|
[platformio]
|
||||||
;
|
crosspoint_version = 0.5.1
|
||||||
; Build options: build flags, source filter
|
default_envs = default
|
||||||
; Upload options: custom upload port, speed and extra flags
|
|
||||||
; Library options: dependencies, extra library storages
|
|
||||||
; Advanced options: extra scripting
|
|
||||||
;
|
|
||||||
; Please visit documentation for the other options and examples
|
|
||||||
; https://docs.platformio.org/page/projectconf.html
|
|
||||||
|
|
||||||
[env:esp32-c3-devkitm-1]
|
[base]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp32-c3-devkitm-1
|
board = esp32-c3-devkitm-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
@@ -19,22 +13,34 @@ board_upload.flash_size = 16MB
|
|||||||
board_upload.maximum_size = 16777216
|
board_upload.maximum_size = 16777216
|
||||||
board_upload.offset_address = 0x10000
|
board_upload.offset_address = 0x10000
|
||||||
|
|
||||||
|
build_flags =
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||||
|
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
||||||
|
-DXML_GE=0
|
||||||
|
-DXML_CONTEXT_BYTES=1024
|
||||||
|
-std=c++2a
|
||||||
|
|
||||||
; Board configuration
|
; Board configuration
|
||||||
board_build.flash_mode = dio
|
board_build.flash_mode = dio
|
||||||
board_build.flash_size = 16MB
|
board_build.flash_size = 16MB
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
|
|
||||||
build_flags =
|
|
||||||
-DARDUINO_USB_MODE=1
|
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
|
||||||
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
|
||||||
# https://libexpat.github.io/doc/api/latest/#XML_GE
|
|
||||||
-DXML_GE=0
|
|
||||||
-DXML_CONTEXT_BYTES=1024
|
|
||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
lib_deps =
|
lib_deps =
|
||||||
zinggjm/GxEPD2@^1.6.5
|
|
||||||
https://github.com/leethomason/tinyxml2.git#11.0.0
|
|
||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
||||||
|
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
||||||
|
|
||||||
|
[env:default]
|
||||||
|
extends = base
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}-dev\"
|
||||||
|
|
||||||
|
[env:gh_release]
|
||||||
|
extends = base
|
||||||
|
build_flags =
|
||||||
|
${base.build_flags}
|
||||||
|
-DCROSSPOINT_VERSION=\"${platformio.crosspoint_version}\"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ bool CrossPointState::loadFromFile() {
|
|||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, version);
|
serialization::readPod(inputFile, version);
|
||||||
if (version != STATE_FILE_VERSION) {
|
if (version != STATE_FILE_VERSION) {
|
||||||
Serial.printf("CrossPointState: Unknown version %u\n", version);
|
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated with:
|
||||||
|
* ruby -rdigest -e 'puts [
|
||||||
|
* "./lib/EpdFont/builtinFonts/bookerly_2b.h",
|
||||||
|
* "./lib/EpdFont/builtinFonts/bookerly_bold_2b.h",
|
||||||
|
* "./lib/EpdFont/builtinFonts/bookerly_bold_italic_2b.h",
|
||||||
|
* "./lib/EpdFont/builtinFonts/bookerly_italic_2b.h",
|
||||||
|
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||||
|
*/
|
||||||
|
#define READER_FONT_ID 1747632454
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated with:
|
||||||
|
* ruby -rdigest -e 'puts [
|
||||||
|
* "./lib/EpdFont/builtinFonts/ubuntu_10.h",
|
||||||
|
* "./lib/EpdFont/builtinFonts/ubuntu_bold_10.h",
|
||||||
|
* ].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||||
|
*/
|
||||||
|
#define UI_FONT_ID 225955604
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated with:
|
||||||
|
* ruby -rdigest -e 'puts [
|
||||||
|
* "./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 2037928017
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
extern const uint8_t CrossLarge[] = {
|
static const uint8_t CrossLarge[] = {
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
|
0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+52
-35
@@ -1,13 +1,21 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <EpdRenderer.h>
|
#include <EInkDisplay.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GxEPD2_BW.h>
|
#include <GfxRenderer.h>
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
|
#include <builtinFonts/bookerly_2b.h>
|
||||||
|
#include <builtinFonts/bookerly_bold_2b.h>
|
||||||
|
#include <builtinFonts/bookerly_bold_italic_2b.h>
|
||||||
|
#include <builtinFonts/bookerly_italic_2b.h>
|
||||||
|
#include <builtinFonts/pixelarial14.h>
|
||||||
|
#include <builtinFonts/ubuntu_10.h>
|
||||||
|
#include <builtinFonts/ubuntu_bold_10.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "config.h"
|
||||||
#include "screens/BootLogoScreen.h"
|
#include "screens/BootLogoScreen.h"
|
||||||
#include "screens/EpubReaderScreen.h"
|
#include "screens/EpubReaderScreen.h"
|
||||||
#include "screens/FileSelectionScreen.h"
|
#include "screens/FileSelectionScreen.h"
|
||||||
@@ -28,32 +36,44 @@
|
|||||||
#define SD_SPI_CS 12
|
#define SD_SPI_CS 12
|
||||||
#define SD_SPI_MISO 7
|
#define SD_SPI_MISO 7
|
||||||
|
|
||||||
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
|
||||||
EPD_RST, EPD_BUSY));
|
|
||||||
InputManager inputManager;
|
InputManager inputManager;
|
||||||
EpdRenderer renderer(display);
|
GfxRenderer renderer(einkDisplay);
|
||||||
Screen* currentScreen;
|
Screen* currentScreen;
|
||||||
CrossPointState appState;
|
CrossPointState appState;
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
EpdFont bookerlyFont(&bookerly_2b);
|
||||||
|
EpdFont bookerlyBoldFont(&bookerly_bold_2b);
|
||||||
|
EpdFont bookerlyItalicFont(&bookerly_italic_2b);
|
||||||
|
EpdFont bookerlyBoldItalicFont(&bookerly_bold_italic_2b);
|
||||||
|
EpdFontFamily bookerlyFontFamily(&bookerlyFont, &bookerlyBoldFont, &bookerlyItalicFont, &bookerlyBoldItalicFont);
|
||||||
|
|
||||||
|
EpdFont smallFont(&pixelarial14);
|
||||||
|
EpdFontFamily smallFontFamily(&smallFont);
|
||||||
|
|
||||||
|
EpdFont ubuntu10Font(&ubuntu_10);
|
||||||
|
EpdFont ubuntuBold10Font(&ubuntu_bold_10);
|
||||||
|
EpdFontFamily ubuntuFontFamily(&ubuntu10Font, &ubuntuBold10Font);
|
||||||
|
|
||||||
// Power button timing
|
// Power button timing
|
||||||
// Time required to confirm boot from sleep
|
// Time required to confirm boot from sleep
|
||||||
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
|
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1000;
|
||||||
// Time required to enter sleep mode
|
// Time required to enter sleep mode
|
||||||
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
||||||
|
|
||||||
Epub* loadEpub(const std::string& path) {
|
std::unique_ptr<Epub> loadEpub(const std::string& path) {
|
||||||
if (!SD.exists(path.c_str())) {
|
if (!SD.exists(path.c_str())) {
|
||||||
Serial.printf("File does not exist: %s\n", path.c_str());
|
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto epub = new Epub(path, "/.crosspoint");
|
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
|
||||||
if (epub->load()) {
|
if (epub->load()) {
|
||||||
return epub;
|
return epub;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.println("Failed to load epub");
|
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
|
||||||
delete epub;
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,15 +95,13 @@ void verifyWakeupLongPress() {
|
|||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
bool abort = false;
|
bool abort = false;
|
||||||
|
|
||||||
Serial.println("Verifying power button press");
|
Serial.printf("[%lu] [ ] Verifying power button press\n", millis());
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
|
while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
|
||||||
delay(50);
|
delay(50);
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
Serial.println("Waiting...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Made it? %s\n", inputManager.isPressed(InputManager::BTN_POWER) ? "yes" : "no");
|
|
||||||
if (inputManager.isPressed(InputManager::BTN_POWER)) {
|
if (inputManager.isPressed(InputManager::BTN_POWER)) {
|
||||||
do {
|
do {
|
||||||
delay(50);
|
delay(50);
|
||||||
@@ -94,8 +112,6 @@ void verifyWakeupLongPress() {
|
|||||||
abort = true;
|
abort = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("held for %lu\n", inputManager.getHeldTime());
|
|
||||||
|
|
||||||
if (abort) {
|
if (abort) {
|
||||||
// Button released too early. Returning to sleep.
|
// Button released too early. Returning to sleep.
|
||||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||||
@@ -117,13 +133,13 @@ void enterDeepSleep() {
|
|||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new SleepScreen(renderer, inputManager));
|
enterNewScreen(new SleepScreen(renderer, inputManager));
|
||||||
|
|
||||||
Serial.println("Power button released after a long press. Entering deep sleep.");
|
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
|
||||||
delay(1000); // Allow Serial buffer to empty and display to update
|
delay(1000); // Allow Serial buffer to empty and display to update
|
||||||
|
|
||||||
// Enable Wakeup on LOW (button press)
|
// Enable Wakeup on LOW (button press)
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
|
||||||
display.hibernate();
|
einkDisplay.deepSleep();
|
||||||
|
|
||||||
// Enter Deep Sleep
|
// Enter Deep Sleep
|
||||||
esp_deep_sleep_start();
|
esp_deep_sleep_start();
|
||||||
@@ -134,15 +150,16 @@ void onSelectEpubFile(const std::string& path) {
|
|||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
|
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
|
||||||
|
|
||||||
Epub* epub = loadEpub(path);
|
auto epub = loadEpub(path);
|
||||||
if (epub) {
|
if (epub) {
|
||||||
appState.openEpubPath = path;
|
appState.openEpubPath = path;
|
||||||
appState.saveToFile();
|
appState.saveToFile();
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
|
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
|
||||||
} else {
|
} else {
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, false, false));
|
enterNewScreen(
|
||||||
|
new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
|
||||||
delay(2000);
|
delay(2000);
|
||||||
onGoHome();
|
onGoHome();
|
||||||
}
|
}
|
||||||
@@ -154,15 +171,13 @@ void onGoHome() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
|
||||||
|
|
||||||
inputManager.begin();
|
inputManager.begin();
|
||||||
verifyWakeupLongPress();
|
verifyWakeupLongPress();
|
||||||
|
|
||||||
// Begin serial only if USB connected
|
|
||||||
pinMode(UART0_RXD, INPUT);
|
|
||||||
if (digitalRead(UART0_RXD) == HIGH) {
|
|
||||||
Serial.begin(115200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize pins
|
// Initialize pins
|
||||||
pinMode(BAT_GPIO0, INPUT);
|
pinMode(BAT_GPIO0, INPUT);
|
||||||
|
|
||||||
@@ -170,11 +185,13 @@ void setup() {
|
|||||||
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
|
|
||||||
// Initialize display
|
// Initialize display
|
||||||
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
einkDisplay.begin();
|
||||||
display.init(115200, true, 2, false, SPI, spi_settings);
|
Serial.printf("[%lu] [ ] Display initialized\n", millis());
|
||||||
display.setRotation(3); // 270 degrees
|
|
||||||
display.setTextColor(GxEPD_BLACK);
|
renderer.insertFont(READER_FONT_ID, bookerlyFontFamily);
|
||||||
Serial.println("Display initialized");
|
renderer.insertFont(UI_FONT_ID, ubuntuFontFamily);
|
||||||
|
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||||
|
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
|
||||||
|
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new BootLogoScreen(renderer, inputManager));
|
enterNewScreen(new BootLogoScreen(renderer, inputManager));
|
||||||
@@ -184,10 +201,10 @@ void setup() {
|
|||||||
|
|
||||||
appState.loadFromFile();
|
appState.loadFromFile();
|
||||||
if (!appState.openEpubPath.empty()) {
|
if (!appState.openEpubPath.empty()) {
|
||||||
Epub* epub = loadEpub(appState.openEpubPath);
|
auto epub = loadEpub(appState.openEpubPath);
|
||||||
if (epub) {
|
if (epub) {
|
||||||
exitScreen();
|
exitScreen();
|
||||||
enterNewScreen(new EpubReaderScreen(renderer, inputManager, epub, onGoHome));
|
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
|
||||||
// Ensure we're not still holding the power button before leaving setup
|
// Ensure we're not still holding the power button before leaving setup
|
||||||
waitForPowerRelease();
|
waitForPowerRelease();
|
||||||
return;
|
return;
|
||||||
@@ -205,8 +222,8 @@ void loop() {
|
|||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
static unsigned long lastMemPrint = 0;
|
static unsigned long lastMemPrint = 0;
|
||||||
if (Serial && millis() - lastMemPrint >= 5000) {
|
if (Serial && millis() - lastMemPrint >= 10000) {
|
||||||
Serial.printf("[%lu] Memory - Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||||
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||||
lastMemPrint = millis();
|
lastMemPrint = millis();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
#include "BootLogoScreen.h"
|
#include "BootLogoScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
#include "images/CrossLarge.h"
|
#include "images/CrossLarge.h"
|
||||||
|
|
||||||
void BootLogoScreen::onEnter() {
|
void BootLogoScreen::onEnter() {
|
||||||
const auto pageWidth = renderer.getPageWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
const auto pageHeight = renderer.getPageHeight();
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
// Location for images is from top right in landscape orientation
|
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
||||||
renderer.drawImage(CrossLarge, (pageHeight - 128) / 2, (pageWidth - 128) / 2, 128, 128);
|
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
|
||||||
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
class BootLogoScreen final : public Screen {
|
class BootLogoScreen final : public Screen {
|
||||||
public:
|
public:
|
||||||
explicit BootLogoScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
#include "EpubReaderScreen.h"
|
#include "EpubReaderScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <Epub/Page.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
|
#include "EpubReaderChapterSelectionScreen.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
constexpr int PAGES_PER_REFRESH = 20;
|
constexpr int PAGES_PER_REFRESH = 15;
|
||||||
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||||
|
constexpr float lineCompression = 0.95f;
|
||||||
|
constexpr int marginTop = 8;
|
||||||
|
constexpr int marginRight = 10;
|
||||||
|
constexpr int marginBottom = 22;
|
||||||
|
constexpr int marginLeft = 10;
|
||||||
|
|
||||||
void EpubReaderScreen::taskTrampoline(void* param) {
|
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<EpubReaderScreen*>(param);
|
auto* self = static_cast<EpubReaderScreen*>(param);
|
||||||
@@ -29,7 +37,7 @@ void EpubReaderScreen::onEnter() {
|
|||||||
f.read(data, 4);
|
f.read(data, 4);
|
||||||
currentSpineIndex = data[0] + (data[1] << 8);
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
nextPageNumber = data[2] + (data[3] << 8);
|
nextPageNumber = data[2] + (data[3] << 8);
|
||||||
Serial.printf("Loaded cache: %d, %d\n", currentSpineIndex, nextPageNumber);
|
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +61,42 @@ void EpubReaderScreen::onExit() {
|
|||||||
}
|
}
|
||||||
vSemaphoreDelete(renderingMutex);
|
vSemaphoreDelete(renderingMutex);
|
||||||
renderingMutex = nullptr;
|
renderingMutex = nullptr;
|
||||||
delete section;
|
section.reset();
|
||||||
section = nullptr;
|
epub.reset();
|
||||||
delete epub;
|
|
||||||
epub = nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderScreen::handleInput() {
|
void EpubReaderScreen::handleInput() {
|
||||||
|
// 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)) {
|
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
@@ -74,6 +111,14 @@ void EpubReaderScreen::handleInput() {
|
|||||||
return;
|
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;
|
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
|
||||||
|
|
||||||
if (skipChapter) {
|
if (skipChapter) {
|
||||||
@@ -81,8 +126,7 @@ void EpubReaderScreen::handleInput() {
|
|||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
nextPageNumber = 0;
|
nextPageNumber = 0;
|
||||||
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
||||||
delete section;
|
section.reset();
|
||||||
section = nullptr;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
@@ -102,8 +146,7 @@ void EpubReaderScreen::handleInput() {
|
|||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
nextPageNumber = UINT16_MAX;
|
nextPageNumber = UINT16_MAX;
|
||||||
currentSpineIndex--;
|
currentSpineIndex--;
|
||||||
delete section;
|
section.reset();
|
||||||
section = nullptr;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -115,8 +158,7 @@ void EpubReaderScreen::handleInput() {
|
|||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
nextPageNumber = 0;
|
nextPageNumber = 0;
|
||||||
currentSpineIndex++;
|
currentSpineIndex++;
|
||||||
delete section;
|
section.reset();
|
||||||
section = nullptr;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@@ -128,7 +170,7 @@ void EpubReaderScreen::displayTaskLoop() {
|
|||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
renderPage();
|
renderScreen();
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
@@ -136,44 +178,63 @@ void EpubReaderScreen::displayTaskLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Failure handling
|
// TODO: Failure handling
|
||||||
void EpubReaderScreen::renderPage() {
|
void EpubReaderScreen::renderScreen() {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
|
// edge case handling for sub-zero spine index
|
||||||
|
if (currentSpineIndex < 0) {
|
||||||
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) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
||||||
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
|
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||||
section = new Section(epub, currentSpineIndex, renderer);
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
if (!section->loadCacheMetadata()) {
|
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||||
Serial.println("Cache not found, building...");
|
marginLeft)) {
|
||||||
|
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
|
||||||
|
|
||||||
{
|
{
|
||||||
const int textWidth = renderer.getTextWidth("Indexing...");
|
const int textWidth = renderer.getTextWidth(READER_FONT_ID, "Indexing...");
|
||||||
constexpr int margin = 20;
|
constexpr int margin = 20;
|
||||||
const int x = (renderer.getPageWidth() - textWidth - margin * 2) / 2;
|
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2;
|
||||||
constexpr int y = 50;
|
constexpr int y = 50;
|
||||||
const int w = textWidth + margin * 2;
|
const int w = textWidth + margin * 2;
|
||||||
const int h = renderer.getLineHeight() + 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.fillRect(x, y, w, h, 0);
|
||||||
renderer.drawText(x + margin, y + margin, "Indexing...");
|
renderer.drawText(READER_FONT_ID, x + margin, y + margin, "Indexing...");
|
||||||
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
||||||
renderer.flushArea(x, y, w, h);
|
renderer.displayBuffer();
|
||||||
|
pagesUntilFullRefresh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section->setupCacheDir();
|
section->setupCacheDir();
|
||||||
if (!section->persistPageDataToSD()) {
|
if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom,
|
||||||
Serial.println("Failed to persist page data to SD");
|
marginLeft)) {
|
||||||
delete section;
|
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
|
||||||
section = nullptr;
|
section.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("Cache found, skipping build...");
|
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPageNumber == UINT16_MAX) {
|
if (nextPageNumber == UINT16_MAX) {
|
||||||
@@ -184,14 +245,34 @@ void EpubReaderScreen::renderPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
section->renderPage();
|
|
||||||
renderStatusBar();
|
if (section->pageCount == 0) {
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
||||||
renderer.flushDisplay(false);
|
renderer.drawCenteredText(READER_FONT_ID, 300, "Empty chapter", true, BOLD);
|
||||||
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
renderStatusBar();
|
||||||
} else {
|
renderer.displayBuffer();
|
||||||
renderer.flushDisplay();
|
return;
|
||||||
pagesUntilFullRefresh--;
|
}
|
||||||
|
|
||||||
|
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
||||||
|
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
|
||||||
|
renderer.drawCenteredText(READER_FONT_ID, 300, "Out of bounds", true, BOLD);
|
||||||
|
renderStatusBar();
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto p = section->loadPageFromSD();
|
||||||
|
if (!p) {
|
||||||
|
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
|
||||||
|
section->clearCache();
|
||||||
|
section.reset();
|
||||||
|
return renderScreen();
|
||||||
|
}
|
||||||
|
const auto start = millis();
|
||||||
|
renderContents(std::move(p));
|
||||||
|
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
||||||
@@ -204,23 +285,56 @@ void EpubReaderScreen::renderPage() {
|
|||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
|
||||||
|
page->render(renderer, READER_FONT_ID);
|
||||||
|
renderStatusBar();
|
||||||
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
pagesUntilFullRefresh = PAGES_PER_REFRESH;
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
pagesUntilFullRefresh--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// grayscale rendering
|
||||||
|
// TODO: Only do this if font supports it
|
||||||
|
{
|
||||||
|
renderer.clearScreen(0x00);
|
||||||
|
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
|
page->render(renderer, READER_FONT_ID);
|
||||||
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
|
|
||||||
|
// Render and copy to MSB buffer
|
||||||
|
renderer.clearScreen(0x00);
|
||||||
|
renderer.setFontRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||||
|
page->render(renderer, READER_FONT_ID);
|
||||||
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
|
|
||||||
|
// display grayscale part
|
||||||
|
renderer.displayGrayBuffer();
|
||||||
|
renderer.setFontRenderMode(GfxRenderer::BW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void EpubReaderScreen::renderStatusBar() const {
|
void EpubReaderScreen::renderStatusBar() const {
|
||||||
const auto pageWidth = renderer.getPageWidth();
|
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 std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||||
const auto progressTextWidth = renderer.getSmallTextWidth(progress.c_str());
|
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
|
||||||
renderer.drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
|
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 uint16_t percentage = battery.readPercentage();
|
||||||
const auto percentageText = std::to_string(percentage) + "%";
|
const auto percentageText = std::to_string(percentage) + "%";
|
||||||
const auto percentageTextWidth = renderer.getSmallTextWidth(percentageText.c_str());
|
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||||
renderer.drawSmallText(20, 765, percentageText.c_str());
|
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageText.c_str());
|
||||||
|
|
||||||
// 1 column on left, 2 columns on right, 5 columns of battery body
|
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||||
constexpr int batteryWidth = 15;
|
constexpr int batteryWidth = 15;
|
||||||
constexpr int batteryHeight = 10;
|
constexpr int batteryHeight = 10;
|
||||||
constexpr int x = 0;
|
constexpr int x = marginLeft;
|
||||||
constexpr int y = 772;
|
constexpr int y = 783;
|
||||||
|
|
||||||
// Top line
|
// Top line
|
||||||
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
||||||
@@ -230,8 +344,8 @@ void EpubReaderScreen::renderStatusBar() const {
|
|||||||
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
||||||
// Battery end
|
// Battery end
|
||||||
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
||||||
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3);
|
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
|
||||||
renderer.drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3);
|
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||||
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||||
|
|
||||||
// The +1 is to round up, so that we always fill at least one pixel
|
// The +1 is to round up, so that we always fill at least one pixel
|
||||||
@@ -241,17 +355,27 @@ void EpubReaderScreen::renderStatusBar() const {
|
|||||||
}
|
}
|
||||||
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
||||||
|
|
||||||
|
// Centered chatper title text
|
||||||
// Page width minus existing content with 30px padding on each side
|
// Page width minus existing content with 30px padding on each side
|
||||||
const int leftMargin = 20 + percentageTextWidth + 30;
|
const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft;
|
||||||
const int rightMargin = progressTextWidth + 30;
|
const int titleMarginRight = progressTextWidth + 30 + marginRight;
|
||||||
const int availableTextWidth = pageWidth - leftMargin - rightMargin;
|
const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight;
|
||||||
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
auto title = tocItem.title;
|
|
||||||
int titleWidth = renderer.getSmallTextWidth(title.c_str());
|
std::string title;
|
||||||
while (titleWidth > availableTextWidth) {
|
int titleWidth;
|
||||||
title = title.substr(0, title.length() - 8) + "...";
|
if (tocIndex == -1) {
|
||||||
titleWidth = renderer.getSmallTextWidth(title.c_str());
|
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.drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
|
renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
#include "Screen.h"
|
#include "Screen.h"
|
||||||
|
|
||||||
class EpubReaderScreen final : public Screen {
|
class EpubReaderScreen final : public Screen {
|
||||||
Epub* epub;
|
std::shared_ptr<Epub> epub;
|
||||||
Section* section = nullptr;
|
std::unique_ptr<Section> section = nullptr;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
std::unique_ptr<Screen> subScreen = nullptr;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int nextPageNumber = 0;
|
int nextPageNumber = 0;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
@@ -20,13 +21,14 @@ class EpubReaderScreen final : public Screen {
|
|||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void renderPage();
|
void renderScreen();
|
||||||
|
void renderContents(std::unique_ptr<Page> p);
|
||||||
void renderStatusBar() const;
|
void renderStatusBar() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpubReaderScreen(EpdRenderer& renderer, InputManager& inputManager, Epub* epub,
|
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
|
||||||
const std::function<void()>& onGoHome)
|
const std::function<void()>& onGoHome)
|
||||||
: Screen(renderer, inputManager), epub(epub), onGoHome(onGoHome) {}
|
: Screen(renderer, inputManager), epub(std::move(epub)), onGoHome(onGoHome) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void handleInput() override;
|
void handleInput() override;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "FileSelectionScreen.h"
|
#include "FileSelectionScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||||
if (str1.back() == '/' && str2.back() != '/') return true;
|
if (str1.back() == '/' && str2.back() != '/') return true;
|
||||||
@@ -51,7 +53,7 @@ void FileSelectionScreen::onEnter() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
||||||
1024, // Stack size
|
2048, // Stack size
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
@@ -118,21 +120,20 @@ void FileSelectionScreen::displayTaskLoop() {
|
|||||||
void FileSelectionScreen::render() const {
|
void FileSelectionScreen::render() const {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const auto pageWidth = renderer.getPageWidth();
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD);
|
renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD);
|
||||||
renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD);
|
|
||||||
|
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
renderer.drawUiText(10, 50, "No EPUBs found");
|
renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found");
|
||||||
} else {
|
} else {
|
||||||
// Draw selection
|
// Draw selection
|
||||||
renderer.fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30);
|
||||||
|
|
||||||
for (size_t i = 0; i < files.size(); i++) {
|
for (size_t i = 0; i < files.size(); i++) {
|
||||||
const auto file = files[i];
|
const auto file = files[i];
|
||||||
renderer.drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1);
|
renderer.drawText(UI_FONT_ID, 20, 60 + i * 30, file.c_str(), i != selectorIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.flushDisplay();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class FileSelectionScreen final : public Screen {
|
|||||||
void loadFiles();
|
void loadFiles();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileSelectionScreen(EpdRenderer& renderer, InputManager& inputManager,
|
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
|
||||||
const std::function<void(const std::string&)>& onSelect)
|
const std::function<void(const std::string&)>& onSelect)
|
||||||
: Screen(renderer, inputManager), onSelect(onSelect) {}
|
: Screen(renderer, inputManager), onSelect(onSelect) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include "FullScreenMessageScreen.h"
|
#include "FullScreenMessageScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
void FullScreenMessageScreen::onEnter() {
|
void FullScreenMessageScreen::onEnter() {
|
||||||
const auto width = renderer.getUiTextWidth(text.c_str(), style);
|
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||||
const auto height = renderer.getLineHeight();
|
const auto top = (GfxRenderer::getScreenHeight() - height) / 2;
|
||||||
const auto left = (renderer.getPageWidth() - width) / 2;
|
|
||||||
const auto top = (renderer.getPageHeight() - height) / 2;
|
|
||||||
|
|
||||||
renderer.clearScreen(invert);
|
renderer.clearScreen();
|
||||||
renderer.drawUiText(left, top, text.c_str(), invert ? 0 : 1, style);
|
renderer.drawCenteredText(UI_FONT_ID, top, text.c_str(), true, style);
|
||||||
renderer.flushDisplay(partialUpdate);
|
renderer.displayBuffer(refreshMode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <EInkDisplay.h>
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "EpdFontFamily.h"
|
|
||||||
#include "Screen.h"
|
#include "Screen.h"
|
||||||
|
|
||||||
class FullScreenMessageScreen final : public Screen {
|
class FullScreenMessageScreen final : public Screen {
|
||||||
std::string text;
|
std::string text;
|
||||||
EpdFontStyle style;
|
EpdFontStyle style;
|
||||||
bool invert;
|
EInkDisplay::RefreshMode refreshMode;
|
||||||
bool partialUpdate;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FullScreenMessageScreen(EpdRenderer& renderer, InputManager& inputManager, std::string text,
|
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
|
||||||
const EpdFontStyle style = REGULAR, const bool invert = false,
|
const EpdFontStyle style = REGULAR,
|
||||||
const bool partialUpdate = true)
|
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||||
: Screen(renderer, inputManager),
|
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
|
||||||
text(std::move(text)),
|
|
||||||
style(style),
|
|
||||||
invert(invert),
|
|
||||||
partialUpdate(partialUpdate) {}
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <InputManager.h>
|
#include <InputManager.h>
|
||||||
|
|
||||||
class EpdRenderer;
|
class GfxRenderer;
|
||||||
|
|
||||||
class Screen {
|
class Screen {
|
||||||
protected:
|
protected:
|
||||||
EpdRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
InputManager& inputManager;
|
InputManager& inputManager;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Screen(EpdRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
|
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
|
||||||
virtual ~Screen() = default;
|
virtual ~Screen() = default;
|
||||||
virtual void onEnter() {}
|
virtual void onEnter() {}
|
||||||
virtual void onExit() {}
|
virtual void onExit() {}
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
#include "SleepScreen.h"
|
#include "SleepScreen.h"
|
||||||
|
|
||||||
#include <EpdRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
#include "images/SleepScreenImg.h"
|
#include "config.h"
|
||||||
|
#include "images/CrossLarge.h"
|
||||||
|
|
||||||
void SleepScreen::onEnter() { renderer.drawImageNoMargin(SleepScreenImg, 0, 0, 800, 480, false, true); }
|
void SleepScreen::onEnter() {
|
||||||
|
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||||
|
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
||||||
|
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
||||||
|
renderer.invertScreen();
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
class SleepScreen final : public Screen {
|
class SleepScreen final : public Screen {
|
||||||
public:
|
public:
|
||||||
explicit SleepScreen(EpdRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user