Backup: Stable state with EPUB reader fixes (Freeze, OOM, Speed, State, Tooling)

This commit is contained in:
Antigravity Agent 2026-01-19 18:44:45 -05:00
parent 21277e03eb
commit 1237f01ac2
34 changed files with 1600 additions and 192 deletions

80
CUSTOM_FONTS.md Normal file
View File

@ -0,0 +1,80 @@
# 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**: Contains metadata (magic "EPDF", version, metrics, offsets).
* **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**: `Family-Style-Size.epdfont`
* Example: `Literata-Regular-14.epdfont`
* Example: `Literata-BoldItalic-14.epdfont`
* **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.

224
SettingsActivity_HEAD.cpp Normal file
View File

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

View File

@ -0,0 +1,323 @@
#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)
: EpdFont(data),
filePath(filePath),
offsetIntervals(offsetIntervals),
offsetGlyphs(offsetGlyphs),
offsetBitmaps(offsetBitmaps) {
// 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) {
// Calculate total glyphs to ensure bounds safety
uint32_t totalGlyphCount = (offsetBitmaps - offsetGlyphs) / 13;
if (glyphIndex >= totalGlyphCount) {
Serial.printf("CustomEpdFont: Glyph index %u out of bounds (total %u)\n", glyphIndex, totalGlyphCount);
// If out of bounds, and we haven't tried fallback, try it.
if (!triedFallback) {
if (currentCp == 0x2018 || currentCp == 0x2019) {
currentCp = 0x0027;
triedFallback = true;
continue;
} else if (currentCp == 0x201C || currentCp == 0x201D) {
currentCp = 0x0022;
triedFallback = true;
continue;
}
}
return nullptr;
}
uint32_t glyphFileOffset = offsetGlyphs + (glyphIndex * 13);
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 glyphBuf[13];
if (fontFile.read(glyphBuf, 13) != 13) {
Serial.println("CustomEpdFont: Read failed (glyph entry)");
fontFile.close();
return nullptr;
}
uint8_t w = glyphBuf[0];
uint8_t h = glyphBuf[1];
uint8_t adv = glyphBuf[2];
int8_t l = (int8_t)glyphBuf[3];
// glyphBuf[4] unused
int8_t t = (int8_t)glyphBuf[5];
// glyphBuf[6] unused
uint16_t dLen = glyphBuf[7] | (glyphBuf[8] << 8);
uint32_t 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;
}
}
return nullptr;
}
return nullptr;
}
const uint8_t* CustomEpdFont::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style) const {
if (!glyph) return nullptr;
// Serial.printf("CustomEpdFont::loadGlyphBitmap glyph=%p len=%u\n", glyph, glyph->dataLength);
if (glyph->dataLength == 0) {
return nullptr; // Empty glyph
}
if (glyph->dataLength > 32768) {
Serial.printf("CustomEpdFont: Glyph too large (%u)\n", glyph->dataLength);
return nullptr;
}
// Serial.printf("[CEF] loadGlyphBitmap: len=%u, off=%u\n", glyph->dataLength, glyph->dataOffset);
uint32_t offset = glyph->dataOffset;
// Check bitmap cache
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].data && bitmapCache[i].codePoint == offset) {
bitmapCache[i].lastAccess = ++currentAccessCount;
if (buffer) {
memcpy(buffer, bitmapCache[i].data, std::min((size_t)glyph->dataLength, (size_t)bitmapCache[i].size));
return buffer;
}
return bitmapCache[i].data;
}
}
// Cache miss - read from SD
if (!fontFile.isOpen()) {
if (!SdMan.openFileForRead("CustomFont", filePath.c_str(), fontFile)) {
Serial.printf("Failed to open font file: %s\n", filePath.c_str());
return nullptr;
}
}
if (!fontFile.seekSet(offsetBitmaps + offset)) {
Serial.printf("CustomEpdFont: Failed to seek to bitmap offset %u\n", offsetBitmaps + offset);
fontFile.close();
return nullptr;
}
// Allocate memory manually
uint8_t* newData = (uint8_t*)malloc(glyph->dataLength);
if (!newData) {
Serial.println("CustomEpdFont: MALLOC FAILED");
fontFile.close();
return nullptr;
}
size_t bytesRead = fontFile.read(newData, glyph->dataLength);
// fontFile.close(); // Keep file open
if (bytesRead != glyph->dataLength) {
Serial.printf("CustomEpdFont: Read mismatch. Expected %u, got %u\n", glyph->dataLength, bytesRead);
free(newData);
return nullptr;
}
// Find slot in bitmap cache (LRU)
int slotIndex = -1;
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].data == nullptr) {
slotIndex = i;
break;
}
}
if (slotIndex == -1) {
uint32_t minAccess = 0xFFFFFFFF;
for (size_t i = 0; i < BITMAP_CACHE_CAPACITY; i++) {
if (bitmapCache[i].lastAccess < minAccess) {
minAccess = bitmapCache[i].lastAccess;
slotIndex = i;
}
}
// Free evicted slot
if (bitmapCache[slotIndex].data) {
free(bitmapCache[slotIndex].data);
bitmapCache[slotIndex].data = nullptr;
}
}
// Store in cache
bitmapCache[slotIndex].codePoint = offset;
bitmapCache[slotIndex].lastAccess = ++currentAccessCount;
bitmapCache[slotIndex].data = newData;
bitmapCache[slotIndex].size = glyph->dataLength;
if (buffer) {
memcpy(buffer, newData, glyph->dataLength);
return buffer;
}
return newData;
}

View File

@ -0,0 +1,50 @@
#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);
~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;
void clearCache() const;
};

View File

@ -1,11 +1,12 @@
#include "EpdFont.h" #include "EpdFont.h"
#include <Arduino.h>
#include <Utf8.h> #include <Utf8.h>
#include <algorithm> #include <algorithm>
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX, 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; *minX = startX;
*minY = startY; *minY = startY;
*maxX = startX; *maxX = startX;
@ -19,15 +20,13 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const int cursorY = startY; const int cursorY = startY;
uint32_t cp; uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
const EpdGlyph* glyph = getGlyph(cp); const EpdGlyph* glyph = getGlyph(cp, style);
if (!glyph) { if (!glyph) {
// TODO: Replace with fallback glyph property? glyph = getGlyph('?', style);
glyph = getGlyph('?');
} }
if (!glyph) { if (!glyph) {
// TODO: Better handle this?
continue; continue;
} }
@ -39,31 +38,32 @@ 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; 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; *w = maxX - minX;
*h = maxY - minY; *h = maxY - minY;
} }
bool EpdFont::hasPrintableChars(const char* string) const { bool EpdFont::hasPrintableChars(const char* string, const EpdFontStyles::Style style) const {
int w = 0, h = 0; int w = 0, h = 0;
getTextDimensions(string, &w, &h); getTextDimensions(string, &w, &h, style);
return w > 0 || h > 0; 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 EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount; const int count = data->intervalCount;
if (count == 0) return nullptr; 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 left = 0;
int right = count - 1; int right = count - 1;
@ -76,10 +76,19 @@ const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
} else if (cp > interval->last) { } else if (cp > interval->last) {
left = mid + 1; left = mid + 1;
} else { } else {
// Found: cp >= interval->first && cp <= interval->last if (data->glyph) {
return &data->glyph[interval->offset + (cp - interval->first)]; return &data->glyph[interval->offset + (cp - interval->first)];
} }
return nullptr;
}
} }
return nullptr; return nullptr;
} }
const uint8_t* EpdFont::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style) const {
const EpdFontData* data = getData(style);
if (!data || !data->bitmap) return nullptr;
return data->bitmap + glyph->dataOffset;
}

View File

@ -1,15 +1,24 @@
#pragma once #pragma once
#include "EpdFontData.h" #include "EpdFontData.h"
#include "EpdFontStyles.h"
class EpdFont { 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: public:
const EpdFontData* data; const EpdFontData* data;
explicit EpdFont(const EpdFontData* data) : data(data) {} explicit EpdFont(const EpdFontData* data) : data(data) {}
~EpdFont() = default; virtual ~EpdFont() = default;
void getTextDimensions(const char* string, int* w, int* h) const;
bool hasPrintableChars(const char* string) const;
const EpdGlyph* getGlyph(uint32_t cp) const; void getTextDimensions(const char* string, int* w, int* h,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
bool hasPrintableChars(const char* string, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const EpdGlyph* getGlyph(uint32_t cp, const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const uint8_t* loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
virtual const EpdFontData* getData(const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const { return data; }
}; };

View File

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

View File

@ -1,24 +1,33 @@
#pragma once #pragma once
#include "EpdFont.h" #include "EpdFont.h"
#include "EpdFontStyles.h"
class EpdFontFamily { class EpdFontFamily {
public: 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, explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr) const EpdFont* boldItalic = nullptr)
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {} : regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default; ~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const; void getTextDimensions(const char* string, int* w, int* h, Style style = EpdFontStyles::REGULAR) const;
bool hasPrintableChars(const char* string, Style style = REGULAR) const; bool hasPrintableChars(const char* string, Style style = EpdFontStyles::REGULAR) const;
const EpdFontData* getData(Style style = REGULAR) const; const EpdFontData* getData(Style style = EpdFontStyles::REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = 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: private:
const EpdFont* regular; const EpdFont* regular;
const EpdFont* bold; const EpdFont* bold;
const EpdFont* italic; const EpdFont* italic;
const EpdFont* boldItalic; const EpdFont* boldItalic;
const EpdFont* getFont(Style style) const;
}; };

View File

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

View File

@ -15,12 +15,14 @@ parser.add_argument("size", type=int, help="font size to use.")
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.") parser.add_argument("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("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.") parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
parser.add_argument("--binary", dest="isBinary", action="store_true", help="output a binary .epdfont file instead of a C header.")
args = parser.parse_args() args = parser.parse_args()
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"]) GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
font_stack = [freetype.Face(f) for f in args.fontstack] font_stack = [freetype.Face(f) for f in args.fontstack]
is2Bit = args.is2Bit is2Bit = args.is2Bit
isBinary = args.isBinary
size = args.size size = args.size
font_name = args.name font_name = args.name
@ -60,45 +62,6 @@ intervals = [
(0x2200, 0x22FF), (0x2200, 0x22FF),
# Arrows # Arrows
(0x2190, 0x21FF), (0x2190, 0x21FF),
### CJK ###
# Core Unified Ideographs
# (0x4E00, 0x9FFF),
# # Extension A
# (0x3400, 0x4DBF),
# # Extension B
# (0x20000, 0x2A6DF),
# # Extension CF
# (0x2A700, 0x2EBEF),
# # Extension G
# (0x30000, 0x3134F),
# # Hiragana
# (0x3040, 0x309F),
# # Katakana
# (0x30A0, 0x30FF),
# # Katakana Phonetic Extensions
# (0x31F0, 0x31FF),
# # Halfwidth Katakana
# (0xFF60, 0xFF9F),
# # Hangul Syllables
# (0xAC00, 0xD7AF),
# # Hangul Jamo
# (0x1100, 0x11FF),
# # Hangul Compatibility Jamo
# (0x3130, 0x318F),
# # Hangul Jamo Extended-A
# (0xA960, 0xA97F),
# # Hangul Jamo Extended-B
# (0xD7B0, 0xD7FF),
# # CJK Radicals Supplement
# (0x2E80, 0x2EFF),
# # Kangxi Radicals
# (0x2F00, 0x2FDF),
# # CJK Symbols and Punctuation
# (0x3000, 0x303F),
# # CJK Compatibility Forms
# (0xFE30, 0xFE4F),
# # CJK Compatibility Ideographs
# (0xF900, 0xFAFF),
] ]
add_ints = [] add_ints = []
@ -200,16 +163,6 @@ for i_start, i_end in intervals:
if (bitmap.width * bitmap.rows) % 4 != 0: if (bitmap.width * bitmap.rows) % 4 != 0:
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2 px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
pixels2b.append(px) 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: else:
# Downsample to 1-bit bitmap - treat any 2+ as black # Downsample to 1-bit bitmap - treat any 2+ as black
pixelsbw = [] pixelsbw = []
@ -228,16 +181,6 @@ for i_start, i_end in intervals:
px = px << (8 - (bitmap.width * bitmap.rows) % 8) px = px << (8 - (bitmap.width * bitmap.rows) % 8)
pixelsbw.append(px) 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 pixels = pixels2b if is2Bit else pixelsbw
# Build output data # Build output data
@ -265,6 +208,51 @@ for index, glyph in enumerate(all_glyphs):
glyph_data.extend([b for b in packed]) glyph_data.extend([b for b in packed])
glyph_props.append(props) glyph_props.append(props)
if isBinary:
import struct
with open(f"{font_name}.epdfont", "wb") as f:
# Magic
f.write(b"EPDF")
# Metrics (22 bytes)
# intervalCount (uint32_t), advanceY (uint8_t), ascender (int32_t), descender (int32_t), is2Bit (uint8_t), totalGlyphCount (uint32_t)
f.write(struct.pack("<IBiiBI", len(intervals), norm_ceil(face.size.height), norm_ceil(face.size.ascender), norm_floor(face.size.descender), 1 if is2Bit else 0, len(glyph_props)))
# Intervals
offset = 0
for i_start, i_end in intervals:
f.write(struct.pack("<III", i_start, i_end, offset))
offset += i_end - i_start + 1
# Glyphs
for g in glyph_props:
# dataOffset (uint32_t), dataLength (uint16_t), width (uint8_t), height (uint8_t), advanceX (uint8_t), left (int8_t), top (int8_t)
# wait, GlyphProps has width, height, advance_x, left, top, data_length, data_offset, code_point
# We need: dataOffset (4), dataLength (2), width (1), height (1), advanceX (1), left (1), top (1) = 11 bytes?
# Let's use 13 bytes as planned to be safer or just 11.
# Original EpdGlyph:
# uint32_t dataOffset;
# uint16_t dataLength;
# uint8_t width;
# uint8_t height;
# uint8_t advanceX;
# int8_t left;
# int8_t top;
# Total: 4+2+1+1+1+1+1 = 11 bytes.
# I will use 13 bytes to align better if needed, but 11 is fine.
# Let's use 13 bytes as per plan: 4+2+1+1+1+1+1 + 2 padding.
# CustomEpdFont.cpp expects:
# glyphBuf[0] = w
# glyphBuf[1] = h
# glyphBuf[2] = adv
# glyphBuf[3] = l (signed)
# glyphBuf[4] = unused
# glyphBuf[5] = t (signed)
# glyphBuf[6] = unused
# glyphBuf[7-8] = dLen
# glyphBuf[9-12] = dOffset
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(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
print("#pragma once") print("#pragma once")
print("#include \"EpdFontData.h\"\n") print("#include \"EpdFontData.h\"\n")
@ -295,3 +283,4 @@ print(f" {norm_ceil(face.size.ascender)},")
print(f" {norm_floor(face.size.descender)},") print(f" {norm_floor(face.size.descender)},")
print(f" {'true' if is2Bit else 'false'},") print(f" {'true' if is2Bit else 'false'},")
print("};") print("};")

View File

@ -0,0 +1,74 @@
#include "EpdFontLoader.h"
#include <HardwareSerial.h>
#include <cstring>
#include <string>
#include "../../src/CrossPointSettings.h"
#include "../../src/managers/FontManager.h"
void EpdFontLoader::loadFontsFromSd(GfxRenderer& renderer) {
// 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);
} else {
Serial.println("Failed to load custom font family");
}
}
}
}
int EpdFontLoader::getBestFontId(const char* familyName, int size) {
if (!familyName || strlen(familyName) == 0) return -1;
// We assume the font is loaded if we are asking for its ID,
// or at least that the ID generation is deterministic.
// The renderer uses the ID to look up the font.
// If we return an ID that isn't inserted, renderer might crash or show nothing.
// So we should ideally check if it's available.
// 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;
return (int)hash;
}

View File

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

View File

@ -319,11 +319,16 @@ bool Epub::clearCache() const {
} }
void Epub::setupCacheDir() const { void Epub::setupCacheDir() const {
if (SdMan.exists(cachePath.c_str())) { // Always try to create, just in case.
return; 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; } const std::string& Epub::getCachePath() const { return cachePath; }

View File

@ -52,6 +52,11 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
uint16_t count; uint16_t count;
serialization::readPod(file, 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++) { for (uint16_t i = 0; i < count; i++) {
uint8_t tag; uint8_t tag;
serialization::readPod(file, tag); serialization::readPod(file, tag);
@ -60,7 +65,7 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
auto pl = PageLine::deserialize(file); auto pl = PageLine::deserialize(file);
page->elements.push_back(std::move(pl)); page->elements.push_back(std::move(pl));
} else { } 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; return nullptr;
} }
} }

View File

@ -11,6 +11,7 @@
constexpr int MAX_COST = std::numeric_limits<int>::max(); constexpr int MAX_COST = std::numeric_limits<int>::max();
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) { void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
// Serial.printf("addWord: %s\n", word.c_str());
if (word.empty()) return; if (word.empty()) return;
words.push_back(std::move(word)); words.push_back(std::move(word));

View File

@ -7,12 +7,13 @@
#include "parsers/ChapterHtmlSlimParser.h" #include "parsers/ChapterHtmlSlimParser.h"
namespace { namespace {
constexpr uint8_t SECTION_FILE_VERSION = 9; constexpr uint8_t SECTION_FILE_VERSION = 10;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t); sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
} // namespace } // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) { uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
SDLock lock;
if (!file) { if (!file) {
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount); Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
return 0; return 0;
@ -23,7 +24,6 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount); Serial.printf("[%lu] [SCT] Failed to serialize page %d\n", millis(), pageCount);
return 0; return 0;
} }
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
pageCount++; pageCount++;
return position; return position;
@ -54,6 +54,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight) { const uint16_t viewportHeight) {
SDLock lock;
if (!SdMan.openFileForRead("SCT", filePath, file)) { if (!SdMan.openFileForRead("SCT", filePath, file)) {
return false; return false;
} }
@ -93,14 +94,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
serialization::readPod(file, pageCount); serialization::readPod(file, pageCount);
file.close(); file.close();
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
return true; return true;
} }
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem) // Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const { bool Section::clearCache() const {
SDLock lock;
if (!SdMan.exists(filePath.c_str())) { if (!SdMan.exists(filePath.c_str())) {
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
return true; return true;
} }
@ -109,7 +110,6 @@ bool Section::clearCache() const {
return false; return false;
} }
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
return true; return true;
} }
@ -117,6 +117,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const std::function<void()>& progressSetupFn, const uint16_t viewportHeight, const std::function<void()>& progressSetupFn,
const std::function<void(int)>& progressFn) { const std::function<void(int)>& progressFn) {
SDLock lock;
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@ -161,8 +162,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
return false; 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 // Only show progress bar for larger chapters where rendering overhead is worth it
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) { if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn(); progressSetupFn();
@ -217,6 +216,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
} }
std::unique_ptr<Page> Section::loadPageFromSectionFile() { std::unique_ptr<Page> Section::loadPageFromSectionFile() {
SDLock lock;
if (!SdMan.openFileForRead("SCT", filePath, file)) { if (!SdMan.openFileForRead("SCT", filePath, file)) {
return nullptr; return nullptr;
} }
@ -224,9 +224,23 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.seek(HEADER_SIZE - sizeof(uint32_t)); file.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t lutOffset; uint32_t lutOffset;
serialization::readPod(file, 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); file.seek(lutOffset + sizeof(uint32_t) * currentPage);
uint32_t pagePos; uint32_t pagePos;
serialization::readPod(file, 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); file.seek(pagePos);
auto page = Page::deserialize(file); auto page = Page::deserialize(file);

View File

@ -16,6 +16,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
auto wordXposIt = wordXpos.begin(); auto wordXposIt = wordXpos.begin();
for (size_t i = 0; i < words.size(); i++) { 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); renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
std::advance(wordIt, 1); std::advance(wordIt, 1);
@ -52,6 +53,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
// Word count // Word count
serialization::readPod(file, wc); 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) // Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
if (wc > 10000) { if (wc > 10000) {
@ -63,7 +65,13 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
words.resize(wc); words.resize(wc);
wordXpos.resize(wc); wordXpos.resize(wc);
wordStyles.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& x : wordXpos) serialization::readPod(file, x);
for (auto& s : wordStyles) serialization::readPod(file, s); for (auto& s : wordStyles) serialization::readPod(file, s);

View File

@ -55,6 +55,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
} }
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { 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); auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
// Middle of skip // Middle of skip
@ -268,9 +269,17 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
} }
bool ChapterHtmlSlimParser::parseAndBuildPages() { bool ChapterHtmlSlimParser::parseAndBuildPages() {
startNewTextBlock((TextBlock::Style)this->paragraphAlignment); SDLock lock;
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); const XML_Parser parser = XML_ParserCreate(nullptr);
if (parser) Serial.printf("[%lu] [EHP] Parser created\n", millis());
int done; int done;
if (!parser) { if (!parser) {
@ -306,6 +315,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
} }
const size_t len = file.read(buf, 1024); const size_t len = file.read(buf, 1024);
// Serial.printf("[%lu] [EHP] Read %d bytes\n", millis(), len);
if (len == 0 && file.available() > 0) { if (len == 0 && file.available() > 0) {
Serial.printf("[%lu] [EHP] File read error\n", millis()); Serial.printf("[%lu] [EHP] File read error\n", millis());
@ -331,6 +341,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
done = file.available() == 0; done = file.available() == 0;
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
Serial.printf("[%lu] [EHP] XML_ParseBuffer returned error\n", millis());
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser), Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser))); XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
@ -340,6 +351,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
file.close(); file.close();
return false; return false;
} }
vTaskDelay(1);
} while (!done); } while (!done);
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing

View File

@ -2,7 +2,13 @@
#include <Utf8.h> #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 { void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
switch (orientation) { switch (orientation) {
@ -101,10 +107,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
// no printable characters // no printable characters
if (!font.hasPrintableChars(text, style)) { if (!font.hasPrintableChars(text, style)) {
Serial.printf("[%lu] [GFX] text '%s' has no printable chars\n", millis(), text);
return; return;
} }
uint32_t cp; uint32_t cp;
const char* p = text;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
renderChar(font, cp, &xpos, &yPos, black, style); renderChar(font, cp, &xpos, &yPos, black, style);
} }
@ -164,8 +172,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
bool isScaled = false; bool isScaled = false;
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f); int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
int cropPixY = std::floor(bitmap.getHeight() * cropY / 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) { if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth()); scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
@ -175,7 +181,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())); scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
isScaled = true; 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) // 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 // IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
@ -446,7 +451,16 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
return 0; 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 { int GfxRenderer::getFontAscenderSize(const int fontId) const {
@ -455,7 +469,13 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
return 0; 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 { int GfxRenderer::getLineHeight(const int fontId) const {
@ -464,7 +484,13 @@ int GfxRenderer::getLineHeight(const int fontId) const {
return 0; 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, void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
@ -595,7 +621,9 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
const int left = glyph->left; const int left = glyph->left;
const int top = glyph->top; 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) { if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) { for (int glyphY = 0; glyphY < height; glyphY++) {
@ -695,8 +723,6 @@ bool GfxRenderer::storeBwBuffer() {
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE); 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; return true;
} }
@ -742,7 +768,6 @@ void GfxRenderer::restoreBwBuffer() {
einkDisplay.cleanupGrayscaleBuffers(frameBuffer); einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
freeBwBufferChunks(); freeBwBufferChunks();
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
} }
/** /**
@ -760,24 +785,25 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
const bool pixelState, const EpdFontFamily::Style style) const { const bool pixelState, const EpdFontFamily::Style style) const {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) { if (!glyph) {
// TODO: Replace with fallback glyph property?
glyph = fontFamily.getGlyph('?', style); glyph = fontFamily.getGlyph('?', style);
} }
// no glyph?
if (!glyph) { if (!glyph) {
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp); Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
return; return;
} }
const int is2Bit = fontFamily.getData(style)->is2Bit; const EpdFont* font = fontFamily.getFont(style);
const uint32_t offset = glyph->dataOffset; 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 width = glyph->width;
const uint8_t height = glyph->height; const uint8_t height = glyph->height;
const int left = glyph->left; const int left = glyph->left;
const uint8_t* bitmap = nullptr; const uint8_t* bitmap = font->loadGlyphBitmap(glyph, nullptr, style);
bitmap = &fontFamily.getData(style)->bitmap[offset];
if (bitmap != nullptr) { if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) { for (int glyphY = 0; glyphY < height; glyphY++) {

View File

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

View File

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

View File

@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 18; constexpr uint8_t SETTINGS_COUNT = 20;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -48,6 +48,8 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, textAntiAliasing); serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, longPressChapterSkip);
serialization::writeString(outputFile, std::string(customFontFamily));
serialization::writePod(outputFile, customFontSize);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -116,6 +118,15 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, longPressChapterSkip); serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) break; 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);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();
@ -191,7 +202,33 @@ int CrossPointSettings::getRefreshFrequency() const {
} }
} }
#include <EpdFontLoader.h>
int CrossPointSettings::getReaderFontId() const { 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) { switch (fontFamily) {
case BOOKERLY: case BOOKERLY:
default: default:

View File

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

View File

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

View File

@ -12,9 +12,15 @@ void BootActivity::onEnter() {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
Serial.println("[Boot] clearScreen done");
renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); 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); 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"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
Serial.println("[Boot] BOOTING text done");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
Serial.println("[Boot] Version text done");
renderer.displayBuffer(); renderer.displayBuffer();
Serial.println("[Boot] displayBuffer done");
} }

View File

@ -59,7 +59,14 @@ void EpubReaderActivity::onEnter() {
if (f.read(data, 4) == 4) { if (f.read(data, 4) == 4) {
currentSpineIndex = data[0] + (data[1] << 8); currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8); nextPageNumber = data[2] + (data[3] << 8);
Serial.printf("[%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(); f.close();
} }
@ -69,8 +76,6 @@ void EpubReaderActivity::onEnter() {
int textSpineIndex = epub->getSpineIndexForTextReference(); int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) { if (textSpineIndex != 0) {
currentSpineIndex = textSpineIndex; currentSpineIndex = textSpineIndex;
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
textSpineIndex);
} }
} }
@ -260,7 +265,7 @@ void EpubReaderActivity::renderScreen() {
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href; 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)); section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
@ -269,8 +274,6 @@ void EpubReaderActivity::renderScreen() {
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight)) { viewportHeight)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions // Progress bar dimensions
constexpr int barWidth = 200; constexpr int barWidth = 200;
constexpr int barHeight = 10; constexpr int barHeight = 10;
@ -319,7 +322,6 @@ void EpubReaderActivity::renderScreen() {
return; return;
} }
} else { } else {
Serial.printf("[%lu] [ERS] Cache found, skipping build...\n", millis());
} }
if (nextPageNumber == UINT16_MAX) { if (nextPageNumber == UINT16_MAX) {
@ -353,11 +355,17 @@ void EpubReaderActivity::renderScreen() {
Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis()); Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
section->clearCache(); section->clearCache();
section.reset(); 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(); const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
} }
FsFile f; FsFile f;
@ -386,11 +394,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
} }
// Save bw buffer to reset buffer state after grayscale data sync // Save bw buffer to reset buffer state after grayscale data sync
renderer.storeBwBuffer(); bool bufferStored = renderer.storeBwBuffer();
// grayscale rendering // grayscale rendering
// TODO: Only do this if font supports it // Only do this if font supports it AND we successfully stored the backup buffer
if (SETTINGS.textAntiAliasing) { if (SETTINGS.textAntiAliasing && bufferStored) {
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);

View File

@ -75,7 +75,7 @@ void FileSelectionActivity::onEnter() {
updateRequired = true; updateRequired = true;
xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask", xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
2048, // Stack size 4096, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
&displayTaskHandle // Task handle &displayTaskHandle // Task handle

View File

@ -0,0 +1,139 @@
#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) ||
mappedInput.wasPressed(MappedInputManager::Button::PageBack)) {
if (selectedIndex > 0) {
selectedIndex--;
if (selectedIndex < scrollOffset) {
scrollOffset = selectedIndex;
update = true;
} else {
update = true;
}
}
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right) ||
mappedInput.wasPressed(MappedInputManager::Button::PageForward)) {
if (selectedIndex < (int)fontFamilies.size() - 1) {
selectedIndex++;
if (selectedIndex >= scrollOffset + itemsPerPage) {
scrollOffset = selectedIndex - itemsPerPage + 1;
update = true;
} else {
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();
// Reload fonts to make sure the newly selected font is loaded
EpdFontLoader::loadFontsFromSd(renderer);
}
onClose();
}
void FontSelectionActivity::render() const {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Font", true, EpdFontFamily::BOLD);
int y = 50;
if (fontFamilies.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No fonts found in /fonts", false);
renderer.drawCenteredText(UI_10_FONT_ID, 150, "Add .epdfont files to SD Card", false);
renderer.displayBuffer(); // ensure update
return;
}
for (int i = 0; i < itemsPerPage; i++) {
int idx = scrollOffset + i;
if (idx >= (int)fontFamilies.size()) break;
// Draw selection box
if (idx == selectedIndex) {
Serial.printf("[FSA] Drawing selected: %s at %d\n", fontFamilies[idx].c_str(), y);
renderer.fillRect(10, y - 2, 460, 24);
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;
}
renderer.displayBuffer();
}

View File

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

View File

@ -1,5 +1,6 @@
#include "SettingsActivity.h" #include "SettingsActivity.h"
#include <EpdFontLoader.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
@ -7,13 +8,14 @@
#include "CalibreSettingsActivity.h" #include "CalibreSettingsActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "FontSelectionActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
#include "fontIds.h" #include "fontIds.h"
// Define the static settings list // Define the static settings list
namespace { namespace {
constexpr int settingsCount = 20; constexpr int settingsCount = 21;
const SettingInfo settingsList[settingsCount] = { const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
@ -31,7 +33,8 @@ const SettingInfo settingsList[settingsCount] = {
{"Prev, Next", "Next, Prev"}), {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily, SettingInfo::Enum("Reader Font Family", &CrossPointSettings::fontFamily,
{"Bookerly", "Noto Sans", "Open Dyslexic"}), {"Bookerly", "Noto Sans", "Open Dyslexic", "Custom"}),
SettingInfo::Action("Set Custom Font Family"),
SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum("Reader Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), SettingInfo::Enum("Reader Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Value("Reader Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
@ -112,6 +115,15 @@ void SettingsActivity::loop() {
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0; selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
updateRequired = true; updateRequired = true;
} }
if (updateRequired) {
// Ensure selected item is in view
if (selectedSettingIndex < scrollOffset) {
scrollOffset = selectedSettingIndex;
} else if (selectedSettingIndex >= scrollOffset + itemsPerPage) {
scrollOffset = selectedSettingIndex - itemsPerPage + 1;
}
}
} }
void SettingsActivity::toggleCurrentSetting() { void SettingsActivity::toggleCurrentSetting() {
@ -121,6 +133,7 @@ void SettingsActivity::toggleCurrentSetting() {
} }
const auto& setting = settingsList[selectedSettingIndex]; const auto& setting = settingsList[selectedSettingIndex];
Serial.printf("[Settings] Toggling: '%s' (Type: %d)\n", setting.name, (int)setting.type);
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer // Toggle the boolean value using the member pointer
@ -129,6 +142,12 @@ void SettingsActivity::toggleCurrentSetting() {
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr); const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
if (strcmp(setting.name, "Reader Font Family") == 0 || strcmp(setting.name, "Reader Font Size") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
EpdFontLoader::loadFontsFromSd(renderer);
xSemaphoreGive(renderingMutex);
}
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { } 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 // Decreasing would also be nice for large ranges I think but oh well can't have everything
const int8_t currentValue = SETTINGS.*(setting.valuePtr); const int8_t currentValue = SETTINGS.*(setting.valuePtr);
@ -155,6 +174,17 @@ void SettingsActivity::toggleCurrentSetting() {
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Set Custom Font Family") == 0) {
Serial.println("[Settings] Launching FontSelectionActivity");
xSemaphoreTake(renderingMutex, portMAX_DELAY);
subActivity.reset(new FontSelectionActivity(renderer, mappedInput, [this] {
subActivity.reset();
updateRequired = true;
}));
subActivity->onEnter();
xSemaphoreGive(renderingMutex);
} else {
Serial.printf("[Settings] Unknown action: %s\n", setting.name);
} }
} else { } else {
// Only toggle if it's a toggle type and has a value pointer // Only toggle if it's a toggle type and has a value pointer
@ -187,28 +217,41 @@ void SettingsActivity::render() const {
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
// Draw selection // Draw selection
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); if (selectedSettingIndex >= scrollOffset && selectedSettingIndex < scrollOffset + itemsPerPage) {
renderer.fillRect(0, 60 + (selectedSettingIndex - scrollOffset) * 30 - 2, pageWidth - 1, 30);
}
// Draw visible settings
for (int i = 0; i < itemsPerPage; i++) {
int index = scrollOffset + i;
if (index >= settingsCount) break;
// Draw all settings
for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings const int settingY = 60 + i * 30; // 30 pixels between settings
// Draw setting name // Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, i != selectedSettingIndex); renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[index].name, index != selectedSettingIndex);
// Draw value based on setting type // Draw value based on setting type
std::string valueText = ""; std::string valueText = "";
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { if (settingsList[index].type == SettingType::TOGGLE && settingsList[index].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr); const bool value = SETTINGS.*(settingsList[index].valuePtr);
valueText = value ? "ON" : "OFF"; valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { } else if (settingsList[index].type == SettingType::ENUM && settingsList[index].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); const uint8_t value = SETTINGS.*(settingsList[index].valuePtr);
valueText = settingsList[i].enumValues[value]; valueText = settingsList[index].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { } else if (settingsList[index].type == SettingType::VALUE && settingsList[index].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); valueText = std::to_string(SETTINGS.*(settingsList[index].valuePtr));
} else if (settingsList[index].type == SettingType::ACTION &&
strcmp(settingsList[index].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()); 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); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(),
index != selectedSettingIndex);
}
} }
// Draw version text above button hints // Draw version text above button hints

View File

@ -49,6 +49,8 @@ class SettingsActivity final : public ActivityWithSubactivity {
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
int selectedSettingIndex = 0; // Currently selected setting int selectedSettingIndex = 0; // Currently selected setting
int scrollOffset = 0; // Index of the first visible setting
static constexpr int itemsPerPage = 25;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);

View File

@ -1,5 +1,6 @@
#include <Arduino.h> #include <Arduino.h>
#include <EInkDisplay.h> #include <EInkDisplay.h>
#include <EpdFontLoader.h>
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <InputManager.h> #include <InputManager.h>
@ -263,15 +264,20 @@ void setupDisplayAndFonts() {
} }
void setup() { 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(); t1 = millis();
// Only start serial if USB connected // Only start serial if USB connected
pinMode(UART0_RXD, INPUT); pinMode(UART0_RXD, INPUT);
if (digitalRead(UART0_RXD) == HIGH) {
Serial.begin(115200);
}
inputManager.begin(); inputManager.begin();
Serial.printf("[%lu] [DBG] inputManager initialized\n", millis());
// Initialize pins // Initialize pins
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
@ -287,21 +293,37 @@ void setup() {
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
return; return;
} }
Serial.printf("[%lu] [DBG] SdMan.begin() success\n", millis());
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
Serial.printf("[%lu] [DBG] SETTINGS loaded\n", millis());
Serial.flush();
// verify power button press duration after we've read settings. // verify power button press duration after we've read settings.
verifyWakeupLongPress(); // verifyWakeupLongPress(); // Disabled for debugging to prevent auto-shutdown
// Serial.printf("[%lu] [DBG] Wakeup long press verified\n", millis());
// First serial output only here to avoid timing inconsistencies for power button press duration verification // 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.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
Serial.flush();
setupDisplayAndFonts(); 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(); exitActivity();
enterNewActivity(new BootActivity(renderer, mappedInputManager)); enterNewActivity(new BootActivity(renderer, mappedInputManager));
Serial.printf("[%lu] [DBG] BootActivity entered\n", millis());
Serial.flush();
APP_STATE.loadFromFile(); APP_STATE.loadFromFile();
Serial.printf("[%lu] [DBG] APP_STATE loaded\n", millis());
if (APP_STATE.openEpubPath.empty()) { if (APP_STATE.openEpubPath.empty()) {
onGoHome(); onGoHome();
} else { } else {
@ -314,6 +336,7 @@ void setup() {
} }
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
Serial.printf("[%lu] [ ] Setup complete\n", millis());
waitForPowerRelease(); waitForPowerRelease();
} }

View File

@ -0,0 +1,212 @@
#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
int firstDash = name.indexOf('-');
if (firstDash > 0) {
String family = name.substring(0, firstDash);
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;
}
uint32_t intervalCount = buf[1];
uint32_t fileSize = buf[2];
uint32_t height = buf[3];
uint32_t glyphCount = buf[4];
int32_t ascender = (int32_t)buf[5];
int32_t descender = (int32_t)buf[7];
uint32_t offsetIntervals = buf[9];
uint32_t offsetGlyphs = buf[10];
uint32_t offsetBitmaps = buf[11];
Serial.printf("[FontMgr] parsed header: intv=%u, glyphs=%u, fileSz=%u, h=%u, asc=%d, desc=%d\n", intervalCount,
glyphCount, fileSize, height, ascender, descender);
Serial.printf("[FontMgr] offsets: intv=%u, gly=%u, bmp=%u\n", offsetIntervals, offsetGlyphs, offsetBitmaps);
// Validation
if (offsetIntervals >= fileSize || offsetGlyphs >= fileSize || offsetBitmaps >= fileSize) {
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->descender = descender;
fontData->is2Bit = (buf[8] != 0);
fontData->bitmap = nullptr;
return new CustomEpdFont(path, fontData, offsetIntervals, offsetGlyphs, offsetBitmaps);
}
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");
CustomEpdFont* bold = loadFontFile(basePath + "Bold-" + sizeStr + ".epdfont");
CustomEpdFont* italic = loadFontFile(basePath + "Italic-" + sizeStr + ".epdfont");
CustomEpdFont* boldItalic = loadFontFile(basePath + "BoldItalic-" + sizeStr + ".epdfont");
if (!regular) {
if (bold) regular = bold;
}
if (regular) {
EpdFontFamily* family = new EpdFontFamily(regular, bold, italic, boldItalic);
loadedFonts[familyName][fontSize] = family;
return family;
}
return nullptr;
}

View File

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