mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
Merge ad7814137c into 3ce11f14ce
This commit is contained in:
commit
1c63440a27
224
SettingsActivity_HEAD.cpp
Normal file
224
SettingsActivity_HEAD.cpp
Normal 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();
|
||||
}
|
||||
@ -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
88
docs/CHANGES.md
Normal 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
83
docs/CUSTOM_FONTS.md
Normal 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
73
docs/FONT_CONVERSION.md
Normal 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.
|
||||
350
lib/EpdFont/CustomEpdFont.cpp
Normal file
350
lib/EpdFont/CustomEpdFont.cpp
Normal 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;
|
||||
}
|
||||
51
lib/EpdFont/CustomEpdFont.h
Normal file
51
lib/EpdFont/CustomEpdFont.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
5
lib/EpdFont/EpdFontStyles.h
Normal file
5
lib/EpdFont/EpdFontStyles.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace EpdFontStyles {
|
||||
enum Style { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
}
|
||||
@ -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("};")
|
||||
|
||||
90
lib/EpdFontLoader/EpdFontLoader.cpp
Normal file
90
lib/EpdFontLoader/EpdFontLoader.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
14
lib/EpdFontLoader/EpdFontLoader.h
Normal file
14
lib/EpdFontLoader/EpdFontLoader.h
Normal 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;
|
||||
};
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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++) {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
|
||||
123
src/activities/settings/FontSelectionActivity.cpp
Normal file
123
src/activities/settings/FontSelectionActivity.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
#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 < (int)fontFamilies.size(); i++) {
|
||||
int idx = scrollOffset + i;
|
||||
|
||||
// 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();
|
||||
}
|
||||
27
src/activities/settings/FontSelectionActivity.h
Normal file
27
src/activities/settings/FontSelectionActivity.h
Normal 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();
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
20
src/main.cpp
20
src/main.cpp
@ -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();
|
||||
}
|
||||
|
||||
|
||||
302
src/managers/FontManager.cpp
Normal file
302
src/managers/FontManager.cpp
Normal 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;
|
||||
}
|
||||
31
src/managers/FontManager.h
Normal file
31
src/managers/FontManager.h
Normal 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;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user