This commit is contained in:
Ruby 2026-01-26 20:10:00 +00:00 committed by GitHub
commit 3e3cf89c27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1892 additions and 132 deletions

224
SettingsActivity_HEAD.cpp Normal file
View File

@ -0,0 +1,224 @@
#include "SettingsActivity.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include <cstring>
#include "CalibreSettingsActivity.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "fontIds.h"
// Define the static settings list
namespace {
constexpr int settingsCount = 20;
const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing),
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}),
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily,
{"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Enum("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}),
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("Check for updates")};
} // namespace
void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop();
}
void SettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item
selectedSettingIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void SettingsActivity::onExit() {
ActivityWithSubactivity::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 SettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
// Handle actions with early return
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleCurrentSetting();
updateRequired = true;
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoHome();
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around)
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down (with wrap around)
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true;
}
}
void SettingsActivity::toggleCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return;
}
const auto& setting = settingsList[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer
const bool currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
// Decreasing would also be nice for large ranges I think but oh well can't have everything
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
// Wrap to minValue if exceeding setting value boundary
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
} else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "Calibre Settings") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Check for updates") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
} else {
// Only toggle if it's a toggle type and has a value pointer
return;
}
// Save settings when they change
SETTINGS.saveToFile();
}
void SettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void SettingsActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex);
// Draw value based on setting type
std::string valueText = "";
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr);
valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen
renderer.displayBuffer();
}

View File

@ -149,6 +149,12 @@ You can customize the sleep screen by placing custom images in specific location
> - Use uncompressed BMP files with 24-bit color depth
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
### 3.7 Custom Fonts
You can load your own custom fonts onto the device by converting them to the required `.epdfont` format.
See the **[Font Conversion Guide](docs/FONT_CONVERSION.md)** for detailed instructions on how to use the conversion tool.
---
## 4. Reading Mode

88
docs/CHANGES.md Normal file
View File

@ -0,0 +1,88 @@
# Comparison with Upstream (crosspoint-reader/master)
This document details the modifications and enhancements made in this fork/branch compared to the upstream master branch (`https://github.com/crosspoint-reader/crosspoint-reader`).
## 1. Custom Font Support Framework
A complete system for loading and rendering custom fonts from the SD card was implemented.
### New Core Components
* **`lib/EpdFontLoader/EpdFontLoader.{cpp,h}`**: A new library responsible for discovering, validating, and ensuring custom fonts are loaded into the renderer. It includes safety fallbacks to the default "Bookerly" font if a custom font fails to load.
* **`src/managers/FontManager.{cpp,h}`**: A singleton manager that scans the `/fonts` directory on the SD card, parses `.epdfont` headers, and creates `EpdFontFamily` instances for valid fonts.
* **`lib/EpdFont/CustomEpdFont.{cpp,h}`**: A new font implementation that reads glyph data directly from the binary `.epdfont` files on the SD card, implementing an LRU cache for bitmaps to optimize RAM usage.
* **`src/activities/settings/FontSelectionActivity.{cpp,h}`**: A new UI screen allowing the user to select from available custom fonts found on the SD card.
* **`lib/EpdFont/EpdFontStyles.h`**: Added to define styles (Regular, Bold, Italic, BoldItalic) for better font family management.
### Tooling Updates
* **`lib/EpdFont/scripts/fontconvert.py`**: Significantly rewritten to generate binary `.epdfont` files with a specific 48-byte header and 13-byte glyph structures required by the new firmware reader. It fixes offset calculations that were broken in the original version.
## 2. EPUB Rendering & Parsing Improvements
The EPUB reader core was modified to improve stability, performance, and memory management.
* **`lib/Epub/Epub/Section.cpp`**:
* Removed `SDLock` usage which was causing compilation issues.
* Cleaned up file I/O operations and caching logic.
* **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**:
* Removed `SDLock` dependency.
* Integrated better progress reporting and memory monitoring logs.
* **`lib/Epub/Epub.cpp`**: Enhanced error handling during book loading.
* **`lib/Epub/Epub/Page.cpp`**: Optimized page serialization/deserialization.
## 3. Graphics Renderer Enhancements
* **`lib/GfxRenderer/GfxRenderer.{cpp,h}`**:
* Updated to support `CustomEpdFont` alongside built-in compiled headers.
* Implemented font ID based lookup that seamlessly handles both built-in and dynamic custom fonts.
* Removed excessive verbose logging to improve performance in production builds.
## 4. Application State & Settings
* **`src/CrossPointSettings.{cpp,h}`**:
* Added persistent storage for the selected `customFontFamily`.
* Updated `getReaderFontId()` to resolve IDs dynamically via `EpdFontLoader` when a custom font is selected.
* **`src/main.cpp`**:
* **CRITICAL FIX**: Re-enabled `verifyWakeupLongPress()` to prevent the device from accidentally powering on when plugged in or bumped.
* Integrated `EpdFontLoader::loadFontsFromSd` into the startup sequence.
## 5. User Interface Updates
* **`src/activities/settings/SettingsActivity.cpp`**: Added the "Reader Font Family" menu option to navigate to the new font selection screen.
* **`src/activities/reader/EpubReaderActivity.cpp`**: Updated to use the dynamic font loading system and respect the user's custom font choice.
## 6. Documentation
* **`CUSTOM_FONTS.md`**: Created detailed developer documentation explaining the architecture of the custom font system.
* **`FONT_CONVERSION.md`**: Added a user guide for converting `.ttf`/`.otf` files to `.epdfont` using the Python script.
* **`USER_GUIDE.md`**: Updated with a new section on Custom Fonts and how to use them.
## Summary of Files Added/Modified
**New Files:**
* `CUSTOM_FONTS.md`
* `FONT_CONVERSION.md`
* `lib/EpdFont/CustomEpdFont.{cpp,h}`
* `lib/EpdFont/EpdFontStyles.h`
* `lib/EpdFontLoader/EpdFontLoader.{cpp,h}`
* `src/activities/settings/FontSelectionActivity.{cpp,h}`
* `src/managers/FontManager.{cpp,h}`
**Modified Files:**
* Core Logic: `src/main.cpp`, `src/CrossPointSettings.{cpp,h}`, `src/CrossPointState.cpp`
* UI: `src/activities/settings/SettingsActivity.{cpp,h}`, `src/activities/reader/EpubReaderActivity.cpp`, `src/activities/reader/FileSelectionActivity.cpp`
* Rendering: `lib/GfxRenderer/GfxRenderer.{cpp,h}`, `lib/EpdFont/EpdFont.{cpp,h}`
* EPUB Engine: `lib/Epub/*` (various files optimized and cleaned)
* Tools: `lib/EpdFont/scripts/fontconvert.py`
### Update: Enhanced Font Discovery & Format Support (2025-01-20)
* **V1 Format Support**: Added full support for the newer V1 `.epdfont` format (32-byte header, uint32 offsets) used by the web-based converter (`epdfont.clev.app`).
* **V0 Format Fix**: Fixed a regression in V0 font loading where the header read was truncated to 32 bytes (instead of 48), restoring support for `LibreBaskerville` and other legacy fonts.
* **Flexible Discovery**: Updated `FontManager` to support `Family_Style_Size` (underscore-separated) naming conventions, enabling compatibility with a wider range of auto-generated filenames.
* **Documentation**: Rewrote `FONT_CONVERSION.md` to cover both the Python script and the new web converter.
### Update: Typographic Rendering Improvements (2026-01-22)
* **Precise Character Spacing**: Implemented `getTextAdvance` to use typographic advance widths instead of visual bounding boxes for layout. This fixes clipping issues with characters like em-dashes.
* **Punctuation Attachment**: Added logic to `ParsedText` to "attach" punctuation (., ,, ;, etc.) to the preceding word, ensuring no visual gap appears between the word and the punctuation mark, even when line breaking occurs.
* **Font Converter Precision**: Updated `lib/EpdFont/scripts/fontconvert.py` to use rounding instead of flooring for advance width calculations and fixed a binary file writing bug, resulting in higher quality generated fonts.

83
docs/CUSTOM_FONTS.md Normal file
View File

@ -0,0 +1,83 @@
# Custom Font Implementation Walkthrough
This document outlines the custom font implementation in the CrossPoint Reader codebase. The system allows users to load custom TrueType/OpenType fonts (converted to a binary format) from an SD card and select them via the settings UI.
## System Overview
The custom font system consists of four main components:
1. **Font Converter (`fontconvert.py`)**: A Python script that pre-processes standard fonts into a custom optimized binary format (`.epdfont`).
2. **Font Manager (`FontManager`)**: Scans the SD card for valid font files and manages loaded font families.
3. **Font Loader (`CustomEpdFont`)**: Handles the low-level reading of the binary format, including on-demand caching of glyph bitmaps to save RAM.
4. **UI & Integration**: A settings activity to select fonts and integration into the main rendering loop.
## 1. Font Conversion & Format
To optimize for the limited RAM of the ESP32 and the specific requirements of E-Ink displays, fonts are not loaded directly as TTF/OTF files. Instead, they are pre-processed.
* **Script**: `lib/EpdFont/scripts/fontconvert.py`
* **Input**: TTF/OTF files.
* **Output**: `.epdfont` binary file.
* **Format Details**:
* **Header**:
* **Version 1 (New)**: 32-byte header, uint32 offsets. Compact and efficient.
* **Version 0 (Legacy)**: 48-byte header, uint16 offsets. Retained for backward compatibility.
* **Intervals**: Unicode ranges supported by the font.
* **Glyphs**: Metrics for each character (width, height, advance, offsets).
* **Bitmaps**: 1-bit or 2-bit (antialiased) pixel data for glyphs.
## 2. Storage & Discovery
Fonts are stored on the SD card in the `/fonts` directory.
* **Location**: `/fonts`
* **Naming Convention**:
* **Standard**: `Family-Style-Size.epdfont` (e.g., `LibreBaskerville-Regular-14.epdfont`)
* **Web Converter**: `Family_Style_Size.epdfont` (e.g., `Aileron_Regular_18.epdfont`)
* **Single File**: `Family.epdfont` (e.g., `Aileron.epdfont`) - automatically detected as Regular style.
* **Manager**: `src/managers/FontManager.cpp`
* **Scans** the `/fonts` directory on startup/demand.
* **Groups** files into `Family -> Size -> Styles (Regular, Bold, Italic, BoldItalic)`.
* Exposes available families to the UI.
## 3. Low-Level Implementation (RAM Optimization)
The core logic resides in `lib/EpdFont/CustomEpdFont.cpp`.
* **Inheritance**: `CustomEpdFont` inherits from `EpdFont`.
* **Metadata in RAM**: When a font is loaded, only the *header* and *glyph metrics* (width, height, etc.) are loaded into RAM.
* **Bitmaps on Disk**: Pixel data remains on the SD card.
* **LRU Cache**: A small Least Recently Used (LRU) cache (`MAX_CACHE_SIZE = 30`) holds frequently used glyph bitmaps in RAM.
* **Hit**: Returns cached bitmap.
* **Miss**: Reads the bitmap from the SD card at the specific offset, caches it, and returns it.
* **Benefit**: Allows using large fonts with extensive character sets (e.g., CJK) without exhausting the ESP32's heap.
## 4. User Interface & Selection
The user selects a font through a dedicated Settings activity.
* **File**: `src/activities/settings/CustomFontSelectionActivity.cpp`
* **Flow**:
1. Lists available font families retrieved from `FontManager`.
2. User selects a family.
3. Selection is saved to `SETTINGS.customFontFamilyName`.
## 5. Main Integration
The selected font is applied during the system startup or when settings change.
* **File**: `src/main.cpp`
* **Function**: `setupDisplayAndFonts()`
* **Logic**:
1. Checks if `SETTINGS.fontFamily` is set to `FONT_CUSTOM`.
2. Calls `FontManager::getInstance().getCustomFontFamily(...)` with the saved name and current font size.
3. If found, the font is dynamically inserted into the global `renderer` with a generated ID.
4. The renderer then uses this font for standard text rendering.
## Code Path Summary
1. **SD Card**: `SD:/fonts/MyFont-Regular-14.epdfont`
2. **Wait**: `FontManager::scanFonts()` finds the file.
3. **Select**: User picks "MyFont" in `CustomFontSelectionActivity`.
4. **Load**: `main.cpp` calls `renderer.insertFont(..., FontManager.getCustomFontFamily("MyFont", 14))`
5. **Render**: `CustomEpdFont::getGlyphBitmap()` fetches pixels from SD -> Cache -> Screen.

73
docs/FONT_CONVERSION.md Normal file
View File

@ -0,0 +1,73 @@
# Font Conversion Guide
To use custom fonts with the CrossPoint Reader, you must convert standard `.ttf` or `.otf` font files into the specific `.epdfont` binary format used by the compiled firmware.
## Supported Formats
The CrossPoint Reader supports two versions of the `.epdfont` format:
1. **Version 1 (Recommended):** The newer, optimized format generated by the web converter.
2. **Version 0 (Legacy):** The original format generated by the Python script.
## Method 1: Web Converter (Recommended)
The easiest way to generate compatible fonts is using the online converter.
1. Go to [https://epdfont.clev.app/](https://epdfont.clev.app/).
2. Select your `.ttf` or `.otf` file.
3. Choose the font size (e.g., 18).
4. Download the generated `.epdfont` file.
### Filename Requirements
The firmware scans for fonts in the `/fonts` directory on the SD card. It attempts to parse the filename to determine the Font Family, Style, and Size.
For best results, rename your downloaded file to match one of these patterns:
* `Family_Style_Size.epdfont` (e.g., `Aileron_Regular_18.epdfont`)
* `Family-Style-Size.epdfont` (e.g., `LibreBaskerville-Bold-14.epdfont`)
**Supported Styles:**
* `Regular`
* `Bold`
* `Italic`
* `BoldItalic`
**Note:** If you download a file named just `Aileron.epdfont`, the reader will try to load it, but using the explicit naming convention above ensures the correct style and size are recognized.
## Method 2: Python Script (Improved)
You can also use the included Python script located at `lib/EpdFont/scripts/fontconvert.py`. This script has been recently updated to ensure high-precision metric calculations (fixing issues with spacing and em-dashes).
### Requirements
- Python 3
- `freetype-py` library (`pip install freetype-py`)
### Usage
Run the script from the project root:
```bash
python3 lib/EpdFont/scripts/fontconvert.py --binary [Family-Style-Size] [Size] [PathToFont]
```
### Arguments
1. `name`: The output filename (without extension). **Convention:** `Family-Style-Size` (e.g. `Bookerly-Regular-12`).
2. `size`: The integer point size (e.g. `12`).
3. `fontstack`: Path to the source font file (e.g. `fonts/Bookerly-Regular.ttf`).
4. `--binary`: **REQUIRED**. Flags the script to output the `.epdfont` binary instead of a C header.
### Example
To convert `Bookerly-Regular.ttf` to a size 12 font:
```bash
python3 lib/EpdFont/scripts/fontconvert.py --binary Bookerly-Regular-12 12 fonts/Bookerly-Regular.ttf
```
This will generate `Bookerly-Regular-12.epdfont` in your current directory.
## Installing on Device
1. Copy your generated `.epdfont` files to the `/fonts` directory on your SD card.
2. Restart the CrossPoint Reader.
3. Go to **Settings** -> **Set Custom Font Family** to select your loaded fonts.

View File

@ -0,0 +1,350 @@
#include "CustomEpdFont.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <algorithm>
CustomEpdFont::CustomEpdFont(const String& filePath, const EpdFontData* data, uint32_t offsetIntervals,
uint32_t offsetGlyphs, uint32_t offsetBitmaps, int version)
: EpdFont(data),
filePath(filePath),
offsetIntervals(offsetIntervals),
offsetGlyphs(offsetGlyphs),
offsetBitmaps(offsetBitmaps),
version(version) {
// Initialize bitmap cache
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
bitmapCache[i].data = nullptr;
bitmapCache[i].size = 0;
bitmapCache[i].codePoint = 0;
bitmapCache[i].lastAccess = 0;
}
// Initialize glyph cache
for (size_t i = 0; i < GLYPH_CACHE_CAPACITY; i++) {
glyphCache[i].codePoint = 0xFFFFFFFF;
glyphCache[i].lastAccess = 0;
}
}
CustomEpdFont::~CustomEpdFont() {
clearCache();
if (fontFile.isOpen()) {
fontFile.close();
}
}
void CustomEpdFont::clearCache() const {
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].data) {
free(bitmapCache[i].data);
bitmapCache[i].data = nullptr;
}
bitmapCache[i].size = 0;
}
}
const EpdGlyph* CustomEpdFont::getGlyph(uint32_t cp, const EpdFontStyles::Style style) const {
// Serial.printf("CustomEpdFont::getGlyph cp=%u style=%d this=%p\n", cp, style, this);
// Check glyph cache first
for (size_t i = 0; i < GLYPH_CACHE_CAPACITY; i++) {
if (glyphCache[i].codePoint == cp) {
glyphCache[i].lastAccess = ++currentAccessCount;
// Serial.printf(" Cache hit: %p\n", &glyphCache[i].glyph);
return &glyphCache[i].glyph;
}
}
const EpdFontData* data = getData(style);
if (!data) {
Serial.println("CustomEpdFont::getGlyph: No data!");
return nullptr;
}
const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount;
uint32_t currentCp = cp;
bool triedFallback = false;
// Loop to allow for fallback attempts
while (true) {
// Check glyph cache first
for (size_t i = 0; i < GLYPH_CACHE_CAPACITY; i++) {
if (glyphCache[i].codePoint == currentCp) {
glyphCache[i].lastAccess = ++currentAccessCount;
// Serial.printf(" Cache hit: %p\n", &glyphCache[i].glyph);
return &glyphCache[i].glyph;
}
}
const EpdFontData* data = getData(style);
if (!data) {
Serial.println("CustomEpdFont::getGlyph: No data!");
return nullptr;
}
const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount;
int left = 0;
int right = count - 1;
bool foundInterval = false;
uint32_t glyphIndex = 0;
const EpdUnicodeInterval* foundIntervalPtr = nullptr;
while (left <= right) {
const int mid = left + (right - left) / 2;
const EpdUnicodeInterval* interval = &intervals[mid];
if (currentCp < interval->first) {
right = mid - 1;
} else if (currentCp > interval->last) {
left = mid + 1;
} else {
// Found interval. Calculate index.
glyphIndex = interval->offset + (currentCp - interval->first);
foundIntervalPtr = interval;
foundInterval = true;
break;
}
}
if (foundInterval) {
uint32_t stride = (version == 1) ? 16 : 13;
uint32_t glyphFileOffset = offsetGlyphs + (glyphIndex * stride);
if (!fontFile.isOpen()) {
if (!SdMan.openFileForRead("CustomFont", filePath.c_str(), fontFile)) {
Serial.printf("CustomEpdFont: Failed to open file %s\n", filePath.c_str());
return nullptr;
}
}
if (!fontFile.seekSet(glyphFileOffset)) {
Serial.printf("CustomEpdFont: Failed to seek to glyph offset %u\n", glyphFileOffset);
fontFile.close();
return nullptr;
}
uint8_t w, h, adv, res = 0;
int16_t l, t = 0;
uint32_t dLen, dOffset = 0;
if (version == 1) {
// New format (16 bytes)
uint8_t glyphBuf[16];
if (fontFile.read(glyphBuf, 16) != 16) {
Serial.println("CustomEpdFont: Read failed (glyph entry v1)");
fontFile.close();
return nullptr;
}
/*
view.setUint8(offset++, glyph.width);
view.setUint8(offset++, glyph.height);
view.setUint8(offset++, glyph.advanceX);
view.setUint8(offset++, 0);
view.setInt16(offset, glyph.left, true);
offset += 2;
view.setInt16(offset, glyph.top, true);
offset += 2;
view.setUint32(offset, glyph.dataLength, true);
offset += 4;
view.setUint32(offset, glyph.dataOffset, true);
offset += 4;
*/
w = glyphBuf[0];
h = glyphBuf[1];
adv = glyphBuf[2];
res = glyphBuf[3];
l = (int16_t)(glyphBuf[4] | (glyphBuf[5] << 8)); // Little endian int16
t = (int16_t)(glyphBuf[6] | (glyphBuf[7] << 8)); // Little endian int16
dLen = glyphBuf[8] | (glyphBuf[9] << 8) | (glyphBuf[10] << 16) | (glyphBuf[11] << 24);
dOffset = glyphBuf[12] | (glyphBuf[13] << 8) | (glyphBuf[14] << 16) | (glyphBuf[15] << 24);
} else {
// Old format (13 bytes)
uint8_t glyphBuf[13];
if (fontFile.read(glyphBuf, 13) != 13) {
Serial.println("CustomEpdFont: Read failed (glyph entry)");
fontFile.close();
return nullptr;
}
w = glyphBuf[0];
h = glyphBuf[1];
adv = glyphBuf[2];
l = (int8_t)glyphBuf[3];
// glyphBuf[4] unused
t = (int8_t)glyphBuf[5];
// glyphBuf[6] unused
dLen = glyphBuf[7] | (glyphBuf[8] << 8);
dOffset = glyphBuf[9] | (glyphBuf[10] << 8) | (glyphBuf[11] << 16) | (glyphBuf[12] << 24);
}
/*
Serial.printf("[CEF] Parsed Glyph %u: Off=%u, Len=%u, W=%u, H=%u, L=%d, T=%d\n",
glyphIndex, dOffset, dLen, w, h, l, t);
*/
// Removed individual reads since we read all 13 bytes
// fontFile.close(); // Keep file open for performance
// Find slot in glyph cache (LRU)
int slotIndex = -1;
uint32_t minAccess = 0xFFFFFFFF;
for (size_t i = 0; i < GLYPH_CACHE_CAPACITY; i++) {
if (glyphCache[i].codePoint == 0xFFFFFFFF) {
slotIndex = i;
break;
}
if (glyphCache[i].lastAccess < minAccess) {
minAccess = glyphCache[i].lastAccess;
slotIndex = i;
}
}
// Populate cache
glyphCache[slotIndex].codePoint = currentCp;
glyphCache[slotIndex].lastAccess = ++currentAccessCount;
glyphCache[slotIndex].glyph.dataOffset = dOffset;
glyphCache[slotIndex].glyph.dataLength = dLen;
glyphCache[slotIndex].glyph.width = w;
glyphCache[slotIndex].glyph.height = h;
glyphCache[slotIndex].glyph.advanceX = adv;
glyphCache[slotIndex].glyph.left = l;
glyphCache[slotIndex].glyph.top = t;
// Serial.printf(" Loaded to cache[%d]: %p\n", slotIndex, &glyphCache[slotIndex].glyph);
return &glyphCache[slotIndex].glyph;
}
// Not found in intervals. Try fallback.
if (!triedFallback) {
if (currentCp == 0x2018 || currentCp == 0x2019) { // Left/Right single quote
currentCp = 0x0027; // ASCII apostrophe
triedFallback = true;
continue; // Retry with fallback CP
} else if (currentCp == 0x201C || currentCp == 0x201D) { // Left/Right double quote
currentCp = 0x0022; // ASCII double quote
triedFallback = true;
continue; // Retry with fallback CP
} else if (currentCp == 160) { // Non-breaking space
currentCp = 32; // Space
triedFallback = true;
continue;
} else if (currentCp == 0x2013 || currentCp == 0x2014) { // En/Em dash
currentCp = 0x002D; // Hyphen-Minus
triedFallback = true;
continue;
}
}
return nullptr;
}
return nullptr;
}
const uint8_t* CustomEpdFont::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style) const {
if (!glyph) return nullptr;
// Serial.printf("CustomEpdFont::loadGlyphBitmap glyph=%p len=%u\n", glyph, glyph->dataLength);
if (glyph->dataLength == 0) {
return nullptr; // Empty glyph
}
if (glyph->dataLength > 32768) {
Serial.printf("CustomEpdFont: Glyph too large (%u)\n", glyph->dataLength);
return nullptr;
}
// Serial.printf("[CEF] loadGlyphBitmap: len=%u, off=%u\n", glyph->dataLength, glyph->dataOffset);
uint32_t offset = glyph->dataOffset;
// Check bitmap cache
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].data && bitmapCache[i].codePoint == offset) {
bitmapCache[i].lastAccess = ++currentAccessCount;
if (buffer) {
memcpy(buffer, bitmapCache[i].data, std::min((size_t)glyph->dataLength, (size_t)bitmapCache[i].size));
return buffer;
}
return bitmapCache[i].data;
}
}
// Cache miss - read from SD
if (!fontFile.isOpen()) {
if (!SdMan.openFileForRead("CustomFont", filePath.c_str(), fontFile)) {
Serial.printf("Failed to open font file: %s\n", filePath.c_str());
return nullptr;
}
}
if (!fontFile.seekSet(offsetBitmaps + offset)) {
Serial.printf("CustomEpdFont: Failed to seek to bitmap offset %u\n", offsetBitmaps + offset);
fontFile.close();
return nullptr;
}
// Allocate memory manually
uint8_t* newData = (uint8_t*)malloc(glyph->dataLength);
if (!newData) {
Serial.println("CustomEpdFont: MALLOC FAILED");
fontFile.close();
return nullptr;
}
size_t bytesRead = fontFile.read(newData, glyph->dataLength);
// fontFile.close(); // Keep file open
if (bytesRead != glyph->dataLength) {
Serial.printf("CustomEpdFont: Read mismatch. Expected %u, got %u\n", glyph->dataLength, bytesRead);
free(newData);
return nullptr;
}
// Find slot in bitmap cache (LRU)
int slotIndex = -1;
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].data == nullptr) {
slotIndex = i;
break;
}
}
if (slotIndex == -1) {
uint32_t minAccess = 0xFFFFFFFF;
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].lastAccess < minAccess) {
minAccess = bitmapCache[i].lastAccess;
slotIndex = i;
}
}
// Free evicted slot
if (bitmapCache[slotIndex].data) {
free(bitmapCache[slotIndex].data);
bitmapCache[slotIndex].data = nullptr;
}
}
// Store in cache
bitmapCache[slotIndex].codePoint = offset;
bitmapCache[slotIndex].lastAccess = ++currentAccessCount;
bitmapCache[slotIndex].data = newData;
bitmapCache[slotIndex].size = glyph->dataLength;
if (buffer) {
memcpy(buffer, newData, glyph->dataLength);
return buffer;
}
return newData;
}

View File

@ -0,0 +1,51 @@
#pragma once
#include <SDCardManager.h>
#include <vector>
#include "EpdFont.h"
struct BitmapCacheEntry {
uint32_t codePoint = 0;
uint32_t lastAccess = 0;
uint8_t* data = nullptr;
uint16_t size = 0;
};
struct GlyphStructCacheEntry {
uint32_t codePoint = 0xFFFFFFFF; // Invalid initial value
uint32_t lastAccess = 0;
EpdGlyph glyph;
};
class CustomEpdFont : public EpdFont {
public:
CustomEpdFont(const String& filePath, const EpdFontData* data, uint32_t offsetIntervals, uint32_t offsetGlyphs,
uint32_t offsetBitmaps, int version = 0);
~CustomEpdFont() override;
const EpdGlyph* getGlyph(uint32_t cp, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const override;
const uint8_t* loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const override;
private:
String filePath;
mutable FsFile fontFile;
uint32_t offsetIntervals;
uint32_t offsetGlyphs;
uint32_t offsetBitmaps;
// Bitmap Cache (Pixel data)
static constexpr size_t BITMAP_CACHE_CAPACITY = 10;
mutable BitmapCacheEntry bitmapCache[BITMAP_CACHE_CAPACITY];
// Glyph Struct Cache (Metadata)
static constexpr size_t GLYPH_CACHE_CAPACITY = 200;
mutable GlyphStructCacheEntry glyphCache[GLYPH_CACHE_CAPACITY];
mutable uint32_t currentAccessCount = 0;
int version = 0;
void clearCache() const;
};

View File

@ -1,11 +1,12 @@
#include "EpdFont.h"
#include <Arduino.h>
#include <Utf8.h>
#include <algorithm>
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
int* maxY) const {
int* maxY, const EpdFontStyles::Style style) const {
*minX = startX;
*minY = startY;
*maxX = startX;
@ -19,14 +20,13 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const int cursorY = startY;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const EpdGlyph* glyph = getGlyph(cp);
const EpdGlyph* glyph = getGlyph(cp, style);
if (!glyph) {
glyph = getGlyph(REPLACEMENT_GLYPH);
}
if (!glyph) {
// TODO: Better handle this?
continue;
}
@ -38,31 +38,51 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
}
}
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
void EpdFont::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyles::Style style) const {
int minX = 0, minY = 0, maxX = 0, maxY = 0;
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY, style);
*w = maxX - minX;
*h = maxY - minY;
}
bool EpdFont::hasPrintableChars(const char* string) const {
int EpdFont::getTextAdvance(const char* string, const EpdFontStyles::Style style) const {
if (string == nullptr || *string == '\0') {
return 0;
}
int advance = 0;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const EpdGlyph* glyph = getGlyph(cp, style);
if (!glyph) {
glyph = getGlyph('?', style);
}
if (glyph) {
advance += glyph->advanceX;
}
}
return advance;
}
bool EpdFont::hasPrintableChars(const char* string, const EpdFontStyles::Style style) const {
int w = 0, h = 0;
getTextDimensions(string, &w, &h);
getTextDimensions(string, &w, &h, style);
return w > 0 || h > 0;
}
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp, const EpdFontStyles::Style style) const {
const EpdFontData* data = getData(style);
if (!data) return nullptr;
const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount;
if (count == 0) return nullptr;
// Binary search for O(log n) lookup instead of O(n)
// Critical for Korean fonts with many unicode intervals
int left = 0;
int right = count - 1;
@ -75,10 +95,19 @@ const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
} else if (cp > interval->last) {
left = mid + 1;
} else {
// Found: cp >= interval->first && cp <= interval->last
return &data->glyph[interval->offset + (cp - interval->first)];
if (data->glyph) {
return &data->glyph[interval->offset + (cp - interval->first)];
}
return nullptr;
}
}
return nullptr;
}
const uint8_t* EpdFont::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style) const {
const EpdFontData* data = getData(style);
if (!data || !data->bitmap) return nullptr;
return data->bitmap + glyph->dataOffset;
}

View File

@ -1,15 +1,25 @@
#pragma once
#include "EpdFontData.h"
#include "EpdFontStyles.h"
class EpdFont {
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
protected:
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
public:
const EpdFontData* data;
explicit EpdFont(const EpdFontData* data) : data(data) {}
~EpdFont() = default;
void getTextDimensions(const char* string, int* w, int* h) const;
bool hasPrintableChars(const char* string) const;
virtual ~EpdFont() = default;
const EpdGlyph* getGlyph(uint32_t cp) const;
void getTextDimensions(const char* string, int* w, int* h,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
int getTextAdvance(const char* string, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
bool hasPrintableChars(const char* string, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const EpdGlyph* getGlyph(uint32_t cp, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const uint8_t* loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const EpdFontData* getData(const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const { return data; }
};

View File

@ -11,7 +11,7 @@ typedef struct {
uint8_t advanceX; ///< Distance to advance cursor (x axis)
int16_t left; ///< X dist from cursor pos to UL corner
int16_t top; ///< Y dist from cursor pos to UL corner
uint16_t dataLength; ///< Size of the font data.
uint32_t dataLength; ///< Size of the font data.
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
} EpdGlyph;

View File

@ -1,13 +1,13 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const {
if (style == BOLD && bold) {
if (style == EpdFontStyles::BOLD && bold) {
return bold;
}
if (style == ITALIC && italic) {
if (style == EpdFontStyles::ITALIC && italic) {
return italic;
}
if (style == BOLD_ITALIC) {
if (style == EpdFontStyles::BOLD_ITALIC) {
if (boldItalic) {
return boldItalic;
}
@ -23,15 +23,28 @@ const EpdFont* EpdFontFamily::getFont(const Style style) const {
}
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const Style style) const {
getFont(style)->getTextDimensions(string, w, h);
getFont(style)->getTextDimensions(string, w, h, style);
}
int EpdFontFamily::getTextAdvance(const char* string, const Style style) const {
return getFont(style)->getTextAdvance(string, style);
}
bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const {
return getFont(style)->hasPrintableChars(string);
return getFont(style)->hasPrintableChars(string, style);
}
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
const EpdFontData* EpdFontFamily::getData(const Style style) const {
const EpdFont* font = getFont(style);
return font ? font->getData(style) : nullptr;
}
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
return getFont(style)->getGlyph(cp);
};
const EpdFont* font = getFont(style);
return font ? font->getGlyph(cp, style) : nullptr;
}
const uint8_t* EpdFontFamily::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer, const Style style) const {
const EpdFont* font = getFont(style);
return font ? font->loadGlyphBitmap(glyph, buffer, style) : nullptr;
}

View File

@ -1,24 +1,34 @@
#pragma once
#include "EpdFont.h"
#include "EpdFontStyles.h"
class EpdFontFamily {
public:
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
typedef EpdFontStyles::Style Style;
static constexpr Style REGULAR = EpdFontStyles::REGULAR;
static constexpr Style BOLD = EpdFontStyles::BOLD;
static constexpr Style ITALIC = EpdFontStyles::ITALIC;
static constexpr Style BOLD_ITALIC = EpdFontStyles::BOLD_ITALIC;
EpdFontFamily() : regular(nullptr), bold(nullptr), italic(nullptr), boldItalic(nullptr) {}
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
bool hasPrintableChars(const char* string, Style style = REGULAR) const;
const EpdFontData* getData(Style style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
void getTextDimensions(const char* string, int* w, int* h, Style style = EpdFontStyles::REGULAR) const;
int getTextAdvance(const char* string, Style style = EpdFontStyles::REGULAR) const;
bool hasPrintableChars(const char* string, Style style = EpdFontStyles::REGULAR) const;
const EpdFontData* getData(Style style = EpdFontStyles::REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = EpdFontStyles::REGULAR) const;
const EpdFont* getFont(Style style = EpdFontStyles::REGULAR) const;
// Helper to load glyph bitmap seamlessly from either static or custom (SD-based) fonts
const uint8_t* loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer, Style style = EpdFontStyles::REGULAR) const;
private:
const EpdFont* regular;
const EpdFont* bold;
const EpdFont* italic;
const EpdFont* boldItalic;
const EpdFont* getFont(Style style) const;
};

View File

@ -0,0 +1,5 @@
#pragma once
namespace EpdFontStyles {
enum Style { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
}

View File

@ -15,12 +15,14 @@ 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("--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("--binary", dest="isBinary", action="store_true", help="output a binary .epdfont file instead of a C header.")
args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack]
is2Bit = args.is2Bit
isBinary = args.isBinary
size = args.size
font_name = args.name
@ -116,6 +118,9 @@ def norm_floor(val):
def norm_ceil(val):
return int(math.ceil(val / (1 << 6)))
def norm_round(val):
return int(round(val / 64.0))
def chunks(l, n):
for i in range(0, len(l), n):
yield l[i:i + n]
@ -205,16 +210,6 @@ for i_start, i_end in intervals:
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 2+ as black
pixelsbw = []
@ -233,16 +228,6 @@ for i_start, i_end in intervals:
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
@ -250,7 +235,7 @@ for i_start, i_end in intervals:
glyph = GlyphProps(
width = bitmap.width,
height = bitmap.rows,
advance_x = norm_floor(face.glyph.advance.x),
advance_x = norm_round(face.glyph.advance.x),
left = face.glyph.bitmap_left,
top = face.glyph.bitmap_top,
data_length = len(packed),
@ -270,33 +255,93 @@ for index, glyph in enumerate(all_glyphs):
glyph_data.extend([b for b in packed])
glyph_props.append(props)
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
print("#pragma once")
print("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
for c in chunks(glyph_data, 16):
print (" " + " ".join(f"0x{b:02X}," for b in c))
print ("};\n");
if isBinary:
import struct
with open(f"{font_name}.epdfont", "wb") as f:
# Custom Header Format (48 bytes total)
# 0 : Magic (4) "EPDF"
# 4 : IntervalCount (4)
# 8 : FileSize (4) - Calculated later
# 12: Height (4)
# 16: GlyphCount (4)
# 20: Ascender (4)
# 24: Reserved (4) - (Previously descender or padding?)
# 28: Descender (4)
# 32: Is2Bit (4)
# 36: OffsetIntervals (4)
# 40: OffsetGlyphs (4)
# 44: OffsetBitmaps (4)
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
for i, g in enumerate(glyph_props):
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
print ("};\n");
header_size = 48
intervals_size = len(intervals) * 12 # 3 * 4 bytes
glyphs_size = len(glyph_props) * 13 # 13 bytes per glyph
bitmaps_size = len(bytes(glyph_data))
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
offset = 0
for i_start, i_end in intervals:
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
offset += i_end - i_start + 1
print ("};\n");
offset_intervals = header_size
offset_glyphs = offset_intervals + intervals_size
offset_bitmaps = offset_glyphs + glyphs_size
file_size = offset_bitmaps + bitmaps_size
# Pack header
f.write(b"EPDF")
f.write(struct.pack("<I", len(intervals))) # 4
f.write(struct.pack("<I", file_size)) # 8
f.write(struct.pack("<I", norm_ceil(face.size.height))) # 12
f.write(struct.pack("<I", len(glyph_props))) # 16
f.write(struct.pack("<i", norm_ceil(face.size.ascender))) # 20
f.write(struct.pack("<i", 0)) # 24 (Reserved/Unknown in C++)
f.write(struct.pack("<i", norm_floor(face.size.descender))) # 28
f.write(struct.pack("<I", 1 if is2Bit else 0)) # 32 (Is2Bit, using 4 bytes to align)
f.write(struct.pack("<I", offset_intervals)) # 36
f.write(struct.pack("<I", offset_glyphs)) # 40
f.write(struct.pack("<I", offset_bitmaps)) # 44
# Intervals
current_offset = 0 # Offset relative to start of bitmaps
for i_start, i_end in intervals:
f.write(struct.pack("<III", i_start, i_end, current_offset))
# Calculate offset increment based on number of glyphs in this interval
# This logic mimics the C++ loop: offset += i_end - i_start + 1
# Note: The 'offset' field in intervals points to the INDEX in the Glyph table, not byte offset.
current_offset += i_end - i_start + 1
# Glyphs
for g in glyph_props:
# 13 bytes per glyph
f.write(struct.pack("<BBB b B b B H I", g.width, g.height, g.advance_x, g.left, 0, g.top, 0, g.data_length, g.data_offset))
# Bitmaps
f.write(bytes(glyph_data))
print(f"Generated {font_name}.epdfont")
else:
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("#include \"EpdFontData.h\"\n")
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
for c in chunks(glyph_data, 16):
print (" " + " ".join(f"0x{b:02X}," for b in c))
print ("};\n");
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
for i, g in enumerate(glyph_props):
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
print ("};\n");
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
offset = 0
for i_start, i_end in intervals:
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
offset += i_end - i_start + 1
print ("};\n");
print(f"static const EpdFontData {font_name} = {{")
print(f" {font_name}Bitmaps,")
print(f" {font_name}Glyphs,")
print(f" {font_name}Intervals,")
print(f" {len(intervals)},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print(f" {'true' if is2Bit else 'false'},")
print("};")
print(f"static const EpdFontData {font_name} = {{")
print(f" {font_name}Bitmaps,")
print(f" {font_name}Glyphs,")
print(f" {font_name}Intervals,")
print(f" {len(intervals)},")
print(f" {norm_ceil(face.size.height)},")
print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},")
print(f" {'true' if is2Bit else 'false'},")
print("};")

View File

@ -0,0 +1,90 @@
#include "EpdFontLoader.h"
#include <HardwareSerial.h>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include "../../src/CrossPointSettings.h"
#include "../../src/managers/FontManager.h"
std::vector<int> EpdFontLoader::loadedCustomIds;
void EpdFontLoader::loadFontsFromSd(GfxRenderer& renderer) {
loadedCustomIds.clear();
// Check settings for custom font
if (SETTINGS.fontFamily == CrossPointSettings::FONT_CUSTOM) {
if (strlen(SETTINGS.customFontFamily) > 0) {
Serial.printf("Loading custom font: %s size %d\n", SETTINGS.customFontFamily, SETTINGS.fontSize);
Serial.flush();
// Map enum size to point size roughly (or use customFontSize if non-zero)
int size = 12; // default
// Map generic sizes (Small, Medium, Large, XL) to likely point sizes if not specified
// Assume standard sizes: 12, 14, 16, 18
switch (SETTINGS.fontSize) {
case CrossPointSettings::SMALL:
size = 12;
break;
case CrossPointSettings::MEDIUM:
size = 14;
break;
case CrossPointSettings::LARGE:
size = 16;
break;
case CrossPointSettings::EXTRA_LARGE:
size = 18;
break;
}
EpdFontFamily* family = FontManager::getInstance().getCustomFontFamily(SETTINGS.customFontFamily, size);
if (family) {
// IDs are usually static consts. For custom font, we need a dynamic ID or reserved ID.
// In main.cpp or somewhere, a range might be reserved or we replace an existing one?
// The stash code in main.cpp step 120 showed:
// "Calculate hash ID manually... int id = (int)hash;"
// "renderer.insertFont(id, *msgFont);"
std::string key = std::string(SETTINGS.customFontFamily) + "-" + std::to_string(size);
uint32_t hash = 5381;
for (char c : key) hash = ((hash << 5) + hash) + c;
int id = (int)hash;
Serial.printf("[FontLoader] Inserting custom font '%s' with ID %d (key: %s)\n", SETTINGS.customFontFamily, id,
key.c_str());
renderer.insertFont(id, *family);
loadedCustomIds.push_back(id);
} else {
Serial.println("Failed to load custom font family");
}
}
}
}
int EpdFontLoader::getBestFontId(const char* familyName, int size) {
if (!familyName || strlen(familyName) == 0) return -1;
// For now, just return the deterministic hash.
std::string key = std::string(familyName) + "-" + std::to_string(size);
uint32_t hash = 5381;
for (char c : key) hash = ((hash << 5) + hash) + c;
int id = (int)hash;
// Verify if the font was actually loaded
bool found = false;
for (int loadedId : loadedCustomIds) {
if (loadedId == id) {
found = true;
break;
}
}
if (found) {
return id;
} else {
return -1; // Fallback to builtin font
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <GfxRenderer.h>
#include <vector>
class EpdFontLoader {
public:
static void loadFontsFromSd(GfxRenderer& renderer);
static int getBestFontId(const char* familyName, int size);
private:
static std::vector<int> loadedCustomIds;
};

View File

@ -320,11 +320,16 @@ bool Epub::clearCache() const {
}
void Epub::setupCacheDir() const {
if (SdMan.exists(cachePath.c_str())) {
return;
// Always try to create, just in case.
if (!SdMan.mkdir(cachePath.c_str())) {
// If mkdir failed, it might already exist. Check if it's a directory.
// SdMan doesn't allow checking type easily without opening.
// But let's log the detailed failure state.
bool exists = SdMan.exists(cachePath.c_str());
Serial.printf("[%lu] [EBP] mkdir failed for %s. Exists? %s\n", millis(), cachePath.c_str(), exists ? "YES" : "NO");
} else {
// Serial.printf("[%lu] [EBP] Created cache directory: %s\n", millis(), cachePath.c_str());
}
SdMan.mkdir(cachePath.c_str());
}
const std::string& Epub::getCachePath() const { return cachePath; }

View File

@ -52,6 +52,11 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
uint16_t count;
serialization::readPod(file, count);
if (count > 1000) {
Serial.printf("[%lu] [PGE] WARNING: Suspicious element count %d\n", millis(), count);
return nullptr;
}
for (uint16_t i = 0; i < count; i++) {
uint8_t tag;
serialization::readPod(file, tag);
@ -60,7 +65,7 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
auto pl = PageLine::deserialize(file);
page->elements.push_back(std::move(pl));
} else {
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u at index %d\n", millis(), tag, i);
return nullptr;
}
}

View File

@ -25,7 +25,6 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
return 0;
}
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++;
return position;
@ -100,14 +99,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, pageCount);
file.close();
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
return true;
}
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const {
if (!SdMan.exists(filePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
return true;
}
@ -116,7 +114,6 @@ bool Section::clearCache() const {
return false;
}
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
return true;
}
@ -169,8 +166,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false;
}
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
// Only show progress bar for larger chapters where rendering overhead is worth it
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn();
@ -233,9 +228,23 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t lutOffset;
serialization::readPod(file, lutOffset);
if (lutOffset > file.size() || lutOffset < HEADER_SIZE) {
Serial.printf("[%lu] [SCT] Invalid LUT offset %u (file size %u)\n", millis(), lutOffset, file.size());
file.close();
return nullptr;
}
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
uint32_t pagePos;
serialization::readPod(file, pagePos);
if (pagePos > file.size()) {
Serial.printf("[%lu] [SCT] Invalid page pos %u for page %d\n", millis(), pagePos, currentPage);
file.close();
return nullptr;
}
file.seek(pagePos);
auto page = Page::deserialize(file);

View File

@ -16,6 +16,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
auto wordXposIt = wordXpos.begin();
for (size_t i = 0; i < words.size(); i++) {
Serial.printf("[%lu] [TXB] Rendering word: %s\n", millis(), wordIt->c_str());
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
std::advance(wordIt, 1);
@ -52,6 +53,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
// Word count
serialization::readPod(file, wc);
Serial.printf("[%lu] [TXB] Deserializing TextBlock: %u words\n", millis(), wc);
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
if (wc > 10000) {
@ -63,7 +65,13 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
words.resize(wc);
wordXpos.resize(wc);
wordStyles.resize(wc);
for (auto& w : words) serialization::readString(file, w);
wordStyles.resize(wc);
int i = 0;
for (auto& w : words) {
if (i % 100 == 0 && i > 0) Serial.printf("[%lu] [TXB] Reading word %d/%d\n", millis(), i, wc);
serialization::readString(file, w);
i++;
}
for (auto& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s);

View File

@ -55,6 +55,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
}
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
// Serial.printf("startElement: %s\n", name);
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
// Middle of skip
@ -253,9 +254,16 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
}
bool ChapterHtmlSlimParser::parseAndBuildPages() {
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
Serial.printf("[%lu] [EHP] parseAndBuildPages start. Heap: %u\n", millis(), ESP.getFreeHeap());
Serial.printf("[%lu] [EHP] Calling startNewTextBlock\n", millis());
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
Serial.printf("[%lu] [EHP] startNewTextBlock returned\n", millis());
Serial.printf("[%lu] [EHP] Creating XML parser\n", millis());
const XML_Parser parser = XML_ParserCreate(nullptr);
if (parser) Serial.printf("[%lu] [EHP] Parser created\n", millis());
int done;
if (!parser) {
@ -291,6 +299,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
}
const size_t len = file.read(buf, 1024);
// Serial.printf("[%lu] [EHP] Read %d bytes\n", millis(), len);
if (len == 0 && file.available() > 0) {
Serial.printf("[%lu] [EHP] File read error\n", millis());
@ -316,6 +325,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
done = file.available() == 0;
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [EHP] XML_ParseBuffer returned error\n", millis());
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
@ -325,6 +335,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
file.close();
return false;
}
vTaskDelay(1);
} while (!done);
XML_StopParser(parser, XML_FALSE); // Stop any pending processing

View File

@ -2,7 +2,13 @@
#include <Utf8.h>
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap[fontId] = font; }
void GfxRenderer::clearCustomFonts(const int startId) {
for (auto it = fontMap.lower_bound(startId); it != fontMap.end();) {
it = fontMap.erase(it);
}
}
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
switch (orientation) {
@ -77,6 +83,15 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF
return w;
}
int GfxRenderer::getTextAdvance(const int fontId, const char* text, const EpdFontFamily::Style style) const {
if (fontMap.count(fontId) == 0) {
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
return 0;
}
return fontMap.at(fontId).getTextAdvance(text, style);
}
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const {
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
@ -101,10 +116,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
// no printable characters
if (!font.hasPrintableChars(text, style)) {
Serial.printf("[%lu] [GFX] text '%s' has no printable chars\n", millis(), text);
return;
}
uint32_t cp;
const char* p = text;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
renderChar(font, cp, &xpos, &yPos, black, style);
}
@ -164,8 +181,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
bool isScaled = false;
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
@ -175,7 +190,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
isScaled = true;
}
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
// Calculate output row size (2 bits per pixel, packed into bytes)
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
@ -447,7 +461,16 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
return 0;
}
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
const EpdGlyph* glyph = fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR);
if (!glyph) {
// Serial.printf("[%lu] [GFX] Font %d (Regular) has no space glyph! Using fallback.\n", millis(), fontId);
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
if (data) {
return data->ascender / 3;
}
return 0;
}
return glyph->advanceX;
}
int GfxRenderer::getFontAscenderSize(const int fontId) const {
@ -456,7 +479,13 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
return 0;
}
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
if (!data) {
Serial.printf("[%lu] [GFX] Font %d (Regular) has no data!\n", millis(), fontId);
return 0;
}
return data->ascender;
}
int GfxRenderer::getLineHeight(const int fontId) const {
@ -465,7 +494,13 @@ int GfxRenderer::getLineHeight(const int fontId) const {
return 0;
}
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
if (!data) {
Serial.printf("[%lu] [GFX] Font %d (Regular) has no data!\n", millis(), fontId);
return 0;
}
return data->advanceY;
}
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
@ -596,7 +631,9 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
const int left = glyph->left;
const int top = glyph->top;
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
// Use loadGlyphBitmap to support both static and custom (SD-based) fonts
uint8_t* buffer = nullptr; // Not used for now, as we expect a pointer or cache
const uint8_t* bitmap = font.loadGlyphBitmap(glyph, buffer, style);
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {
@ -696,8 +733,6 @@ bool GfxRenderer::storeBwBuffer() {
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
}
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
BW_BUFFER_CHUNK_SIZE);
return true;
}
@ -743,7 +778,6 @@ void GfxRenderer::restoreBwBuffer() {
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks();
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
}
/**
@ -764,20 +798,22 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, 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 EpdFont* font = fontFamily.getFont(style);
if (!font) return;
const EpdFontData* data = font->getData(style);
if (!data) return;
const int is2Bit = data->is2Bit;
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];
const uint8_t* bitmap = font->loadGlyphBitmap(glyph, nullptr, style);
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {

View File

@ -2,8 +2,10 @@
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <EpdFontStyles.h>
#include <map>
#include <string>
#include "Bitmap.h"
@ -46,6 +48,7 @@ class GfxRenderer {
// Setup
void insertFont(int fontId, EpdFontFamily font);
void clearCustomFonts(int startId = 1000);
// Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation = o; }
@ -72,16 +75,17 @@ class GfxRenderer {
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
// Text
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
int getTextAdvance(int fontId, const char* text, EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
int getSpaceWidth(int fontId) const;
int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const;
std::string truncatedText(int fontId, const char* text, int maxWidth,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
// UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
@ -90,7 +94,7 @@ class GfxRenderer {
private:
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
int getTextHeight(int fontId) const;
public:

View File

@ -39,14 +39,26 @@ static void writeString(FsFile& file, const std::string& s) {
static void readString(std::istream& is, std::string& s) {
uint32_t len;
readPod(is, len);
if (len > 4096) {
s = "";
return;
}
s.resize(len);
is.read(&s[0], len);
if (len > 0) {
is.read(&s[0], len);
}
}
static void readString(FsFile& file, std::string& s) {
uint32_t len;
readPod(file, len);
if (len > 4096) {
s = "";
return;
}
s.resize(len);
file.read(&s[0], len);
if (len > 0) {
file.read(&s[0], len);
}
}
} // namespace serialization

View File

@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 20;
constexpr uint8_t SETTINGS_COUNT = 22;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@ -48,6 +48,8 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip);
serialization::writeString(outputFile, std::string(customFontFamily));
serialization::writePod(outputFile, customFontSize);
serialization::writePod(outputFile, hyphenationEnabled);
outputFile.close();
@ -118,6 +120,14 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break;
{
std::string fontStr;
serialization::readString(inputFile, fontStr);
strncpy(customFontFamily, fontStr.c_str(), sizeof(customFontFamily) - 1);
customFontFamily[sizeof(customFontFamily) - 1] = '\0';
}
if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, customFontSize);
serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
@ -195,7 +205,33 @@ int CrossPointSettings::getRefreshFrequency() const {
}
}
#include <EpdFontLoader.h>
int CrossPointSettings::getReaderFontId() const {
if (fontFamily == FONT_CUSTOM) {
uint8_t targetSize = customFontSize;
if (targetSize == 0) {
switch (fontSize) {
case SMALL:
targetSize = 12;
break;
case MEDIUM:
default:
targetSize = 14;
break;
case LARGE:
targetSize = 16;
break;
case EXTRA_LARGE:
targetSize = 18;
break;
}
}
int id = EpdFontLoader::getBestFontId(customFontFamily, targetSize);
if (id != -1) return id;
// Fallback if custom font not found
}
switch (fontFamily) {
case BOOKERLY:
default:

View File

@ -40,7 +40,7 @@ class CrossPointSettings {
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
// Font family options
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_CUSTOM = 3 };
// Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
@ -77,7 +77,9 @@ class CrossPointSettings {
uint8_t sideButtonLayout = PREV_NEXT;
// Reader font settings
uint8_t fontFamily = BOOKERLY;
char customFontFamily[64] = "";
uint8_t fontSize = MEDIUM;
uint8_t customFontSize = 0; // 0 means use enum mapping
uint8_t lineSpacing = NORMAL;
uint8_t paragraphAlignment = JUSTIFIED;
// Auto-sleep timeout setting (default 10 minutes)

View File

@ -38,7 +38,9 @@ bool CrossPointState::loadFromFile() {
return false;
}
Serial.printf("[%lu] [CPS] Reading OpenEpubPath\n", millis());
serialization::readString(inputFile, openEpubPath);
Serial.printf("[%lu] [CPS] Read OpenEpubPath: %s\n", millis(), openEpubPath.c_str());
if (version >= 2) {
serialization::readPod(inputFile, lastSleepImage);
} else {

View File

@ -12,9 +12,15 @@ void BootActivity::onEnter() {
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
Serial.println("[Boot] clearScreen done");
renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128);
Serial.println("[Boot] drawImage done");
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
Serial.println("[Boot] CrossPoint text done");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
Serial.println("[Boot] BOOTING text done");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
Serial.println("[Boot] Version text done");
renderer.displayBuffer();
Serial.println("[Boot] displayBuffer done");
}

View File

@ -110,7 +110,7 @@ void MyLibraryActivity::loadFiles() {
char name[500];
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
file.getName(name, sizeof(name));
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0 || strcmp(name, "fonts") == 0) {
file.close();
continue;
}

View File

@ -60,7 +60,14 @@ void EpubReaderActivity::onEnter() {
if (f.read(data, 4) == 4) {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber);
// Validation: If loaded index is invalid, reset to 0
if (currentSpineIndex >= epub->getSpineItemsCount()) {
Serial.printf("[%lu] [ERS] Loaded invalid spine index %d (max %d), resetting\n", millis(), currentSpineIndex,
epub->getSpineItemsCount());
currentSpineIndex = 0;
nextPageNumber = 0;
}
}
f.close();
}
@ -70,8 +77,6 @@ void EpubReaderActivity::onEnter() {
int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) {
currentSpineIndex = textSpineIndex;
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
}
}
@ -274,7 +279,7 @@ void EpubReaderActivity::renderScreen() {
if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
@ -333,7 +338,6 @@ void EpubReaderActivity::renderScreen() {
return;
}
} else {
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
}
if (nextPageNumber == UINT16_MAX) {
@ -367,11 +371,17 @@ void EpubReaderActivity::renderScreen() {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache();
section.reset();
return renderScreen();
// Prevent infinite recursion. If load fails, show error.
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Error loading page", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 330, "File system error or corruption", true);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
return;
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
}
FsFile f;
@ -400,11 +410,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
// Save bw buffer to reset buffer state after grayscale data sync
renderer.storeBwBuffer();
bool bufferStored = renderer.storeBwBuffer();
// grayscale rendering
// TODO: Only do this if font supports it
if (SETTINGS.textAntiAliasing) {
// Only do this if font supports it AND we successfully stored the backup buffer
if (SETTINGS.textAntiAliasing && bufferStored) {
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);

View File

@ -11,6 +11,7 @@
#include "KOReaderSettingsActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "FontSelectionActivity.h"
#include "fontIds.h"
void CategorySettingsActivity::taskTrampoline(void* param) {
@ -127,6 +128,14 @@ void CategorySettingsActivity::toggleCurrentSetting() {
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Set Custom Font Family") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new FontSelectionActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
} else {
return;
@ -176,6 +185,11 @@ void CategorySettingsActivity::render() const {
valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
} else if (settingsList[i].type == SettingType::ACTION &&
strcmp(settingsList[i].name, "Set Custom Font Family") == 0) {
if (SETTINGS.fontFamily == CrossPointSettings::FONT_CUSTOM) {
valueText = SETTINGS.customFontFamily;
}
}
if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());

View File

@ -0,0 +1,124 @@
#include "FontSelectionActivity.h"
#include <EpdFontLoader.h>
#include <HardwareSerial.h>
#include "../../CrossPointSettings.h"
#include "../../fontIds.h"
#include "../../managers/FontManager.h"
FontSelectionActivity::FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& inputManager,
std::function<void()> onClose)
: Activity("Font Selection", renderer, inputManager), onClose(onClose) {}
FontSelectionActivity::~FontSelectionActivity() {}
void FontSelectionActivity::onEnter() {
Serial.println("[FSA] onEnter start");
Activity::onEnter();
Serial.println("[FSA] Getting available families...");
fontFamilies = FontManager::getInstance().getAvailableFamilies();
Serial.printf("[FSA] Got %d families\n", fontFamilies.size());
std::string current = SETTINGS.customFontFamily;
Serial.printf("[FSA] Current setting: %s\n", current.c_str());
for (size_t i = 0; i < fontFamilies.size(); i++) {
if (fontFamilies[i] == current) {
selectedIndex = i;
Serial.printf("[FSA] Found current family at index %d\n", i);
// Adjust scroll
if (selectedIndex >= itemsPerPage) {
scrollOffset = selectedIndex - itemsPerPage / 2;
if (scrollOffset > (int)fontFamilies.size() - itemsPerPage) {
scrollOffset = std::max(0, (int)fontFamilies.size() - itemsPerPage);
}
}
break;
}
}
Serial.println("[FSA] Calling render()");
render();
Serial.println("[FSA] onEnter end");
}
void FontSelectionActivity::loop() {
bool update = false;
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onClose();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex > 0) ? (selectedIndex - 1) : ((int)fontFamilies.size() - 1);
update = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex < (int)fontFamilies.size() - 1) ? (selectedIndex + 1) : 0;
update = true;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
saveAndExit();
return;
}
if (update) {
render();
}
}
void FontSelectionActivity::saveAndExit() {
if (selectedIndex >= 0 && selectedIndex < (int)fontFamilies.size()) {
strncpy(SETTINGS.customFontFamily, fontFamilies[selectedIndex].c_str(), sizeof(SETTINGS.customFontFamily) - 1);
SETTINGS.customFontFamily[sizeof(SETTINGS.customFontFamily) - 1] = '\0';
SETTINGS.fontFamily = CrossPointSettings::FONT_CUSTOM;
SETTINGS.saveToFile();
}
onClose();
}
void FontSelectionActivity::render() const {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Font", true, EpdFontFamily::BOLD);
int y = 50;
if (fontFamilies.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No fonts found in /fonts", false);
renderer.drawCenteredText(UI_10_FONT_ID, 150, "Add .epdfont files to SD Card", false);
renderer.displayBuffer(); // ensure update
return;
}
for (int i = 0; i < itemsPerPage; i++) {
int idx = scrollOffset + i;
if (idx >= (int)fontFamilies.size()) break;
// Draw selection box
if (idx == selectedIndex) {
Serial.printf("[FSA] Drawing selected: %s at %d\n", fontFamilies[idx].c_str(), y);
renderer.fillRect(0, y - 2, 480, 30);
renderer.drawText(UI_10_FONT_ID, 20, y, fontFamilies[idx].c_str(), false); // false = white (on black box)
} else {
Serial.printf("[FSA] Drawing: %s at %d\n", fontFamilies[idx].c_str(), y);
renderer.drawText(UI_10_FONT_ID, 20, y, fontFamilies[idx].c_str(), true); // true = black (on white bg)
}
// Mark current active font
if (fontFamilies[idx] == SETTINGS.customFontFamily) {
renderer.drawText(UI_10_FONT_ID, 400, y, "*", idx != selectedIndex);
}
y += 30;
}
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <GfxRenderer.h>
#include <MappedInputManager.h>
#include <functional>
#include <string>
#include <vector>
#include "../Activity.h"
class FontSelectionActivity : public Activity {
public:
FontSelectionActivity(GfxRenderer& renderer, MappedInputManager& inputManager, std::function<void()> onClose);
~FontSelectionActivity() override;
void onEnter() override;
void loop() override;
void render() const;
private:
std::function<void()> onClose;
std::vector<std::string> fontFamilies;
int selectedIndex = 0;
int scrollOffset = 0;
static constexpr int itemsPerPage = 8;
void saveAndExit();
};

View File

@ -1,10 +1,12 @@
#include "SettingsActivity.h"
#include <EpdFontLoader.h>
#include <GfxRenderer.h>
#include <HardwareSerial.h>
#include "CategorySettingsActivity.h"
#include "CrossPointSettings.h"
#include "FontSelectionActivity.h"
#include "MappedInputManager.h"
#include "fontIds.h"
@ -21,9 +23,10 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9;
constexpr int readerSettingsCount = 10;
const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic", "Custom"}),
SettingInfo::Action("Set Custom Font Family"),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
@ -102,6 +105,8 @@ void SettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
// Reload fonts to make sure the newly selected font settings are loaded
EpdFontLoader::loadFontsFromSd(renderer);
onGoHome();
return;
}

View File

@ -1,5 +1,6 @@
#include <Arduino.h>
#include <EInkDisplay.h>
#include <EpdFontLoader.h>
#include <Epub.h>
#include <GfxRenderer.h>
#include <InputManager.h>
@ -289,6 +290,12 @@ bool isWakeupAfterFlashing() {
}
void setup() {
// force serial for debugging
Serial.begin(115200);
delay(500);
Serial.printf("[%lu] [DBG] setup() start - FIRMWARE DEBUG BUILD 001\n", millis());
Serial.flush();
t1 = millis();
// Only start serial if USB connected
@ -303,6 +310,8 @@ void setup() {
}
inputManager.begin();
Serial.printf("[%lu] [DBG] inputManager initialized\n", millis());
// Initialize pins
pinMode(BAT_GPIO0, INPUT);
@ -318,6 +327,7 @@ void setup() {
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
return;
}
Serial.printf("[%lu] [DBG] SdMan.begin() success\n", millis());
SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile();
@ -329,11 +339,20 @@ void setup() {
// First serial output only here to avoid timing inconsistencies for power button press duration verification
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
Serial.flush();
setupDisplayAndFonts();
Serial.printf("[%lu] [DBG] setupDisplayAndFonts done\n", millis());
Serial.flush();
EpdFontLoader::loadFontsFromSd(renderer);
Serial.printf("[%lu] [DBG] loadFontsFromSd done\n", millis());
Serial.flush();
exitActivity();
enterNewActivity(new BootActivity(renderer, mappedInputManager));
Serial.printf("[%lu] [DBG] BootActivity entered\n", millis());
Serial.flush();
APP_STATE.loadFromFile();
RECENT_BOOKS.loadFromFile();
@ -350,6 +369,7 @@ void setup() {
}
// Ensure we're not still holding the power button before leaving setup
Serial.printf("[%lu] [ ] Setup complete\n", millis());
waitForPowerRelease();
}

View File

@ -0,0 +1,302 @@
#include "FontManager.h"
#include <GfxRenderer.h> // for EpdFontData usage validation if needed
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <algorithm>
#include "CustomEpdFont.h"
FontManager& FontManager::getInstance() {
static FontManager instance;
return instance;
}
FontManager::~FontManager() {
for (auto& familyPair : loadedFonts) {
for (auto& sizePair : familyPair.second) {
delete sizePair.second;
}
}
}
const std::vector<std::string>& FontManager::getAvailableFamilies() {
if (!scanned) {
scanFonts();
}
return availableFamilies;
}
void FontManager::scanFonts() {
Serial.println("[FM] Scanning fonts...");
availableFamilies.clear();
scanned = true;
FsFile fontDir;
if (!SdMan.openFileForRead("FontScan", "/fonts", fontDir)) {
Serial.println("[FM] Failed to open /fonts directory");
// Even if failed, we proceed to sort empty list to avoid crashes
return;
}
if (!fontDir.isDirectory()) {
Serial.println("[FM] /fonts is not a directory");
fontDir.close();
return;
}
Serial.println("[FM] /fonts opened. Iterating files...");
FsFile file;
while (file.openNext(&fontDir, O_READ)) {
if (!file.isDirectory()) {
char filename[128];
file.getName(filename, sizeof(filename));
Serial.printf("[FM] Checking: %s\n", filename);
String name = String(filename);
if (name.endsWith(".epdfont")) {
// Expected format: Family-Style-Size.epdfont or Family_Size.epdfont
// Or just Family.epdfont (V1 single file)
String family;
int separator = name.indexOf('-');
if (separator < 0) separator = name.indexOf('_');
if (separator > 0) {
family = name.substring(0, separator);
} else {
// No separator, take the whole name (minus .epdfont)
family = name.substring(0, name.length() - 8);
}
if (family.length() > 0) {
if (std::find(availableFamilies.begin(), availableFamilies.end(), family.c_str()) ==
availableFamilies.end()) {
availableFamilies.push_back(family.c_str());
Serial.printf("[FM] Added family: %s\n", family.c_str());
}
}
}
}
file.close();
}
fontDir.close();
std::sort(availableFamilies.begin(), availableFamilies.end());
Serial.printf("[FM] Scan complete. Found %d families\n", availableFamilies.size());
}
struct EpdfHeader {
char magic[4];
uint32_t intervalCount;
uint32_t totalGlyphCount;
uint8_t advanceY;
int32_t ascender;
int32_t descender;
uint8_t is2Bit;
};
// Helper to load a single font file
CustomEpdFont* loadFontFile(const String& path) {
Serial.printf("[FontMgr] Loading file: %s\n", path.c_str());
Serial.flush();
FsFile f;
if (!SdMan.openFileForRead("FontLoading", path.c_str(), f)) {
Serial.printf("[FontMgr] Failed to open: %s\n", path.c_str());
Serial.flush();
return nullptr;
}
// Read custom header format (detected from file dump)
// 0: Magic (4)
// 4: IntervalCount (4)
// 8: FileSize (4)
// 12: Height (4) -> advanceY
// 16: GlyphCount (4)
// 20: Ascender (4)
// 24: Unknown (4)
// 28: Descender (4)
// 32: Unknown (4)
// 36: OffsetIntervals (4)
// 40: OffsetGlyphs (4)
// 44: OffsetBitmaps (4)
uint32_t buf[12]; // 48 bytes
if (f.read(buf, 48) != 48) {
Serial.printf("[FontMgr] Header read failed for %s\n", path.c_str());
f.close();
return nullptr;
}
if (strncmp((char*)&buf[0], "EPDF", 4) != 0) {
Serial.printf("[FontMgr] Invalid magic for %s\n", path.c_str());
f.close();
return nullptr;
}
Serial.printf("[FontMgr] Header Dump %s: ", path.c_str());
for (int i = 0; i < 12; i++) Serial.printf("%08X ", buf[i]);
Serial.println();
/*
* Version Detection Improved
*
* V1:
* Offset 20 (buf[5]) is OffsetIntervals. It matches header size = 32.
* Offset 4 (buf[1]) low 16 bits is Version = 1.
*
* V0:
* Offset 36 (buf[9]) is OffsetIntervals. It matches header size = 48.
*/
int version = -1;
// Check for V1
if (buf[5] == 32 && (buf[1] & 0xFFFF) == 1) {
version = 1;
}
// Check for V0
else if (buf[9] == 48) {
version = 0;
}
// Fallback: Use the old file size check if offsets are weird (detected from legacy files?)
else if (buf[2] > 10000) {
// V0 has fileSize at offset 8 (buf[2])
version = 0;
}
uint32_t intervalCount, fileSize, glyphCount, offsetIntervals, offsetGlyphs, offsetBitmaps;
uint8_t height, advanceY;
int32_t ascender, descender;
bool is2Bit;
if (version == 1) {
// V1 Parsing
uint8_t* b8 = (uint8_t*)buf;
is2Bit = (b8[6] != 0);
advanceY = b8[8];
ascender = (int8_t)b8[9];
descender = (int8_t)b8[10];
intervalCount = b8[12] | (b8[13] << 8) | (b8[14] << 16) | (b8[15] << 24);
glyphCount = b8[16] | (b8[17] << 8) | (b8[18] << 16) | (b8[19] << 24);
offsetIntervals = b8[20] | (b8[21] << 8) | (b8[22] << 16) | (b8[23] << 24);
offsetGlyphs = b8[24] | (b8[25] << 8) | (b8[26] << 16) | (b8[27] << 24);
offsetBitmaps = b8[28] | (b8[29] << 8) | (b8[30] << 16) | (b8[31] << 24);
height = advanceY;
fileSize = 0; // Unknown
} else if (version == 0) {
// V0 Parsing
// We already read 48 bytes into buf
intervalCount = buf[1];
fileSize = buf[2];
height = buf[3];
glyphCount = buf[4];
ascender = (int32_t)buf[5];
descender = (int32_t)buf[7];
is2Bit = (buf[8] != 0);
offsetIntervals = buf[9];
offsetGlyphs = buf[10];
offsetBitmaps = buf[11];
} else {
Serial.printf("[FontMgr] Unknown version for %s\n", path.c_str());
f.close();
return nullptr;
}
// Validation
// For V1, we trust offsets generated by the tool
if (offsetIntervals == 0 || offsetGlyphs == 0 || offsetBitmaps == 0) {
Serial.println("[FontMgr] Invalid offsets in header");
f.close();
return nullptr;
}
// We need to load intervals into RAM
EpdUnicodeInterval* intervals = new (std::nothrow) EpdUnicodeInterval[intervalCount];
if (!intervals) {
Serial.printf("[FontMgr] Failed to allocate intervals: %d\n", intervalCount);
f.close();
return nullptr;
}
if (!f.seekSet(offsetIntervals)) {
Serial.println("[FontMgr] Failed to seek to intervals");
delete[] intervals;
f.close();
return nullptr;
}
f.read((uint8_t*)intervals, intervalCount * sizeof(EpdUnicodeInterval));
f.close();
// Create EpdFontData
EpdFontData* fontData = new (std::nothrow) EpdFontData();
if (!fontData) {
Serial.println("[FontMgr] Failed to allocate EpdFontData! OOM.");
delete[] intervals;
return nullptr;
}
fontData->intervalCount = intervalCount;
fontData->intervals = intervals;
fontData->glyph = nullptr;
fontData->advanceY = (uint8_t)height;
fontData->ascender = ascender;
fontData->descender = descender;
fontData->is2Bit = is2Bit;
fontData->bitmap = nullptr;
return new CustomEpdFont(path, fontData, offsetIntervals, offsetGlyphs, offsetBitmaps, version);
}
EpdFontFamily* FontManager::getCustomFontFamily(const std::string& familyName, int fontSize) {
if (loadedFonts[familyName][fontSize]) {
return loadedFonts[familyName][fontSize];
}
String basePath = "/fonts/" + String(familyName.c_str()) + "-";
String sizeStr = String(fontSize);
CustomEpdFont* regular = loadFontFile(basePath + "Regular-" + sizeStr + ".epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Regular_" + sizeStr + ".epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + "_" + sizeStr + ".epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + ".epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + "-Regular.epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Regular.epdfont");
if (!regular) regular = loadFontFile("/fonts/" + String(familyName.c_str()) + "-" + sizeStr + ".epdfont");
CustomEpdFont* bold = loadFontFile(basePath + "Bold-" + sizeStr + ".epdfont");
if (!bold) bold = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Bold_" + sizeStr + ".epdfont");
if (!bold) bold = loadFontFile("/fonts/" + String(familyName.c_str()) + "-Bold.epdfont");
if (!bold) bold = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Bold.epdfont");
CustomEpdFont* italic = loadFontFile(basePath + "Italic-" + sizeStr + ".epdfont");
if (!italic) italic = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Italic_" + sizeStr + ".epdfont");
if (!italic) italic = loadFontFile("/fonts/" + String(familyName.c_str()) + "-Italic.epdfont");
if (!italic) italic = loadFontFile("/fonts/" + String(familyName.c_str()) + "_Italic.epdfont");
CustomEpdFont* boldItalic = loadFontFile(basePath + "BoldItalic-" + sizeStr + ".epdfont");
if (!boldItalic)
boldItalic = loadFontFile("/fonts/" + String(familyName.c_str()) + "_BoldItalic_" + sizeStr + ".epdfont");
if (!boldItalic) boldItalic = loadFontFile("/fonts/" + String(familyName.c_str()) + "-BoldItalic.epdfont");
if (!boldItalic) boldItalic = loadFontFile("/fonts/" + String(familyName.c_str()) + "_BoldItalic.epdfont");
if (!regular) {
if (bold) regular = bold;
}
if (regular) {
EpdFontFamily* family = new EpdFontFamily(regular, bold, italic, boldItalic);
loadedFonts[familyName][fontSize] = family;
return family;
}
return nullptr;
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include "EpdFontFamily.h"
class FontManager {
public:
static FontManager& getInstance();
// Scan SD card for fonts
void scanFonts();
// Get list of available font family names
const std::vector<std::string>& getAvailableFamilies();
// Load a specific family and size (returns pointer to cached family or new one)
EpdFontFamily* getCustomFontFamily(const std::string& familyName, int fontSize);
private:
FontManager() = default;
~FontManager();
std::vector<std::string> availableFamilies;
bool scanned = false;
// Map: FamilyName -> Size -> EpdFontFamily*
std::map<std::string, std::map<int, EpdFontFamily*>> loadedFonts;
};