Backup: Stable state with EPUB reader fixes (Freeze, OOM, Speed, State, Tooling)
This commit is contained in:
323
lib/EpdFont/CustomEpdFont.cpp
Normal file
323
lib/EpdFont/CustomEpdFont.cpp
Normal 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;
|
||||
}
|
||||
50
lib/EpdFont/CustomEpdFont.h
Normal file
50
lib/EpdFont/CustomEpdFont.h
Normal 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;
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "EpdFont.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
||||
int* maxY) const {
|
||||
int* maxY, const EpdFontStyles::Style style) const {
|
||||
*minX = startX;
|
||||
*minY = startY;
|
||||
*maxX = startX;
|
||||
@@ -19,15 +20,13 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
const int cursorY = startY;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
const EpdGlyph* glyph = getGlyph(cp, style);
|
||||
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = getGlyph('?');
|
||||
glyph = getGlyph('?', style);
|
||||
}
|
||||
|
||||
if (!glyph) {
|
||||
// TODO: Better handle this?
|
||||
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;
|
||||
|
||||
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY, style);
|
||||
|
||||
*w = maxX - minX;
|
||||
*h = maxY - minY;
|
||||
}
|
||||
|
||||
bool EpdFont::hasPrintableChars(const char* string) const {
|
||||
bool EpdFont::hasPrintableChars(const char* string, const EpdFontStyles::Style style) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
getTextDimensions(string, &w, &h);
|
||||
getTextDimensions(string, &w, &h, style);
|
||||
|
||||
return w > 0 || h > 0;
|
||||
}
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp, const EpdFontStyles::Style style) const {
|
||||
const EpdFontData* data = getData(style);
|
||||
if (!data) return nullptr;
|
||||
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
const int count = data->intervalCount;
|
||||
|
||||
if (count == 0) return nullptr;
|
||||
|
||||
// Binary search for O(log n) lookup instead of O(n)
|
||||
// Critical for Korean fonts with many unicode intervals
|
||||
int left = 0;
|
||||
int right = count - 1;
|
||||
|
||||
@@ -76,10 +76,19 @@ const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
} else if (cp > interval->last) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
// Found: cp >= interval->first && cp <= interval->last
|
||||
return &data->glyph[interval->offset + (cp - interval->first)];
|
||||
if (data->glyph) {
|
||||
return &data->glyph[interval->offset + (cp - interval->first)];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* EpdFont::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer,
|
||||
const EpdFontStyles::Style style) const {
|
||||
const EpdFontData* data = getData(style);
|
||||
if (!data || !data->bitmap) return nullptr;
|
||||
return data->bitmap + glyph->dataOffset;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
#include "EpdFontStyles.h"
|
||||
|
||||
class EpdFont {
|
||||
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
|
||||
protected:
|
||||
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY,
|
||||
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
|
||||
|
||||
public:
|
||||
const EpdFontData* data;
|
||||
explicit EpdFont(const EpdFontData* data) : data(data) {}
|
||||
~EpdFont() = default;
|
||||
void getTextDimensions(const char* string, int* w, int* h) const;
|
||||
bool hasPrintableChars(const char* string) const;
|
||||
virtual ~EpdFont() = default;
|
||||
|
||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||
void getTextDimensions(const char* string, int* w, int* h,
|
||||
const EpdFontStyles::Style style = EpdFontStyles::REGULAR) const;
|
||||
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; }
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
if (style == BOLD && bold) {
|
||||
if (style == EpdFontStyles::BOLD && bold) {
|
||||
return bold;
|
||||
}
|
||||
if (style == ITALIC && italic) {
|
||||
if (style == EpdFontStyles::ITALIC && italic) {
|
||||
return italic;
|
||||
}
|
||||
if (style == BOLD_ITALIC) {
|
||||
if (style == EpdFontStyles::BOLD_ITALIC) {
|
||||
if (boldItalic) {
|
||||
return boldItalic;
|
||||
}
|
||||
@@ -23,15 +23,24 @@ const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
}
|
||||
|
||||
void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const Style style) const {
|
||||
getFont(style)->getTextDimensions(string, w, h);
|
||||
getFont(style)->getTextDimensions(string, w, h, style);
|
||||
}
|
||||
|
||||
bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const {
|
||||
return getFont(style)->hasPrintableChars(string);
|
||||
return getFont(style)->hasPrintableChars(string, style);
|
||||
}
|
||||
|
||||
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
|
||||
const EpdFontData* EpdFontFamily::getData(const Style style) const {
|
||||
const EpdFont* font = getFont(style);
|
||||
return font ? font->getData(style) : nullptr;
|
||||
}
|
||||
|
||||
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {
|
||||
return getFont(style)->getGlyph(cp);
|
||||
};
|
||||
const EpdFont* font = getFont(style);
|
||||
return font ? font->getGlyph(cp, style) : nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* EpdFontFamily::loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer, const Style style) const {
|
||||
const EpdFont* font = getFont(style);
|
||||
return font ? font->loadGlyphBitmap(glyph, buffer, style) : nullptr;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
#pragma once
|
||||
#include "EpdFont.h"
|
||||
#include "EpdFontStyles.h"
|
||||
|
||||
class EpdFontFamily {
|
||||
public:
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
typedef EpdFontStyles::Style Style;
|
||||
static constexpr Style REGULAR = EpdFontStyles::REGULAR;
|
||||
static constexpr Style BOLD = EpdFontStyles::BOLD;
|
||||
static constexpr Style ITALIC = EpdFontStyles::ITALIC;
|
||||
static constexpr Style BOLD_ITALIC = EpdFontStyles::BOLD_ITALIC;
|
||||
|
||||
EpdFontFamily() : regular(nullptr), bold(nullptr), italic(nullptr), boldItalic(nullptr) {}
|
||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||
const EpdFont* boldItalic = nullptr)
|
||||
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
|
||||
~EpdFontFamily() = default;
|
||||
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
|
||||
bool hasPrintableChars(const char* string, Style style = REGULAR) const;
|
||||
const EpdFontData* getData(Style style = REGULAR) const;
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
|
||||
void getTextDimensions(const char* string, int* w, int* h, Style style = EpdFontStyles::REGULAR) const;
|
||||
bool hasPrintableChars(const char* string, Style style = EpdFontStyles::REGULAR) const;
|
||||
const EpdFontData* getData(Style style = EpdFontStyles::REGULAR) const;
|
||||
const EpdGlyph* getGlyph(uint32_t cp, Style style = EpdFontStyles::REGULAR) const;
|
||||
|
||||
const EpdFont* getFont(Style style = EpdFontStyles::REGULAR) const;
|
||||
|
||||
// Helper to load glyph bitmap seamlessly from either static or custom (SD-based) fonts
|
||||
const uint8_t* loadGlyphBitmap(const EpdGlyph* glyph, uint8_t* buffer, Style style = EpdFontStyles::REGULAR) const;
|
||||
|
||||
private:
|
||||
const EpdFont* regular;
|
||||
const EpdFont* bold;
|
||||
const EpdFont* italic;
|
||||
const EpdFont* boldItalic;
|
||||
|
||||
const EpdFont* getFont(Style style) const;
|
||||
};
|
||||
|
||||
5
lib/EpdFont/EpdFontStyles.h
Normal file
5
lib/EpdFont/EpdFontStyles.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace EpdFontStyles {
|
||||
enum Style { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
}
|
||||
@@ -15,12 +15,14 @@ parser.add_argument("size", type=int, help="font size to use.")
|
||||
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
||||
parser.add_argument("--2bit", dest="is2Bit", action="store_true", help="generate 2-bit greyscale bitmap instead of 1-bit black and white.")
|
||||
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
||||
parser.add_argument("--binary", dest="isBinary", action="store_true", help="output a binary .epdfont file instead of a C header.")
|
||||
args = parser.parse_args()
|
||||
|
||||
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "data_length", "data_offset", "code_point"])
|
||||
|
||||
font_stack = [freetype.Face(f) for f in args.fontstack]
|
||||
is2Bit = args.is2Bit
|
||||
isBinary = args.isBinary
|
||||
size = args.size
|
||||
font_name = args.name
|
||||
|
||||
@@ -60,45 +62,6 @@ intervals = [
|
||||
(0x2200, 0x22FF),
|
||||
# Arrows
|
||||
(0x2190, 0x21FF),
|
||||
### CJK ###
|
||||
# Core Unified Ideographs
|
||||
# (0x4E00, 0x9FFF),
|
||||
# # Extension A
|
||||
# (0x3400, 0x4DBF),
|
||||
# # Extension B
|
||||
# (0x20000, 0x2A6DF),
|
||||
# # Extension C–F
|
||||
# (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 = []
|
||||
@@ -200,16 +163,6 @@ for i_start, i_end in intervals:
|
||||
if (bitmap.width * bitmap.rows) % 4 != 0:
|
||||
px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2
|
||||
pixels2b.append(px)
|
||||
|
||||
# for y in range(bitmap.rows):
|
||||
# line = ''
|
||||
# for x in range(bitmap.width):
|
||||
# pixelPosition = y * bitmap.width + x
|
||||
# byte = pixels2b[pixelPosition // 4]
|
||||
# bit_index = (3 - (pixelPosition % 4)) * 2
|
||||
# line += '#' if ((byte >> bit_index) & 3) > 0 else '.'
|
||||
# print(line)
|
||||
# print('')
|
||||
else:
|
||||
# Downsample to 1-bit bitmap - treat any 2+ as black
|
||||
pixelsbw = []
|
||||
@@ -228,16 +181,6 @@ for i_start, i_end in intervals:
|
||||
px = px << (8 - (bitmap.width * bitmap.rows) % 8)
|
||||
pixelsbw.append(px)
|
||||
|
||||
# for y in range(bitmap.rows):
|
||||
# line = ''
|
||||
# for x in range(bitmap.width):
|
||||
# pixelPosition = y * bitmap.width + x
|
||||
# byte = pixelsbw[pixelPosition // 8]
|
||||
# bit_index = 7 - (pixelPosition % 8)
|
||||
# line += '#' if (byte >> bit_index) & 1 else '.'
|
||||
# print(line)
|
||||
# print('')
|
||||
|
||||
pixels = pixels2b if is2Bit else pixelsbw
|
||||
|
||||
# Build output data
|
||||
@@ -265,33 +208,79 @@ for index, glyph in enumerate(all_glyphs):
|
||||
glyph_data.extend([b for b in packed])
|
||||
glyph_props.append(props)
|
||||
|
||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
|
||||
print("#pragma once")
|
||||
print("#include \"EpdFontData.h\"\n")
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||
for c in chunks(glyph_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
print ("};\n");
|
||||
if isBinary:
|
||||
import struct
|
||||
with open(f"{font_name}.epdfont", "wb") as f:
|
||||
# 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("#pragma once")
|
||||
print("#include \"EpdFontData.h\"\n")
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||
for c in chunks(glyph_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
|
||||
for i, g in enumerate(glyph_props):
|
||||
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
|
||||
print ("};\n");
|
||||
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
|
||||
for i, g in enumerate(glyph_props):
|
||||
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
|
||||
offset = 0
|
||||
for i_start, i_end in intervals:
|
||||
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
|
||||
offset += i_end - i_start + 1
|
||||
print ("};\n");
|
||||
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
|
||||
offset = 0
|
||||
for i_start, i_end in intervals:
|
||||
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
|
||||
offset += i_end - i_start + 1
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdFontData {font_name} = {{")
|
||||
print(f" {font_name}Bitmaps,")
|
||||
print(f" {font_name}Glyphs,")
|
||||
print(f" {font_name}Intervals,")
|
||||
print(f" {len(intervals)},")
|
||||
print(f" {norm_ceil(face.size.height)},")
|
||||
print(f" {norm_ceil(face.size.ascender)},")
|
||||
print(f" {norm_floor(face.size.descender)},")
|
||||
print(f" {'true' if is2Bit else 'false'},")
|
||||
print("};")
|
||||
|
||||
print(f"static const EpdFontData {font_name} = {{")
|
||||
print(f" {font_name}Bitmaps,")
|
||||
print(f" {font_name}Glyphs,")
|
||||
print(f" {font_name}Intervals,")
|
||||
print(f" {len(intervals)},")
|
||||
print(f" {norm_ceil(face.size.height)},")
|
||||
print(f" {norm_ceil(face.size.ascender)},")
|
||||
print(f" {norm_floor(face.size.descender)},")
|
||||
print(f" {'true' if is2Bit else 'false'},")
|
||||
print("};")
|
||||
|
||||
74
lib/EpdFontLoader/EpdFontLoader.cpp
Normal file
74
lib/EpdFontLoader/EpdFontLoader.cpp
Normal 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;
|
||||
}
|
||||
9
lib/EpdFontLoader/EpdFontLoader.h
Normal file
9
lib/EpdFontLoader/EpdFontLoader.h
Normal 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);
|
||||
};
|
||||
@@ -319,11 +319,16 @@ bool Epub::clearCache() const {
|
||||
}
|
||||
|
||||
void Epub::setupCacheDir() const {
|
||||
if (SdMan.exists(cachePath.c_str())) {
|
||||
return;
|
||||
// Always try to create, just in case.
|
||||
if (!SdMan.mkdir(cachePath.c_str())) {
|
||||
// If mkdir failed, it might already exist. Check if it's a directory.
|
||||
// SdMan doesn't allow checking type easily without opening.
|
||||
// But let's log the detailed failure state.
|
||||
bool exists = SdMan.exists(cachePath.c_str());
|
||||
Serial.printf("[%lu] [EBP] mkdir failed for %s. Exists? %s\n", millis(), cachePath.c_str(), exists ? "YES" : "NO");
|
||||
} else {
|
||||
// Serial.printf("[%lu] [EBP] Created cache directory: %s\n", millis(), cachePath.c_str());
|
||||
}
|
||||
|
||||
SdMan.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||
|
||||
@@ -52,6 +52,11 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
uint16_t count;
|
||||
serialization::readPod(file, count);
|
||||
|
||||
if (count > 1000) {
|
||||
Serial.printf("[%lu] [PGE] WARNING: Suspicious element count %d\n", millis(), count);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
uint8_t tag;
|
||||
serialization::readPod(file, tag);
|
||||
@@ -60,7 +65,7 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else {
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u\n", millis(), tag);
|
||||
Serial.printf("[%lu] [PGE] Deserialization failed: Unknown tag %u at index %d\n", millis(), tag, i);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
|
||||
// Serial.printf("addWord: %s\n", word.c_str());
|
||||
if (word.empty()) return;
|
||||
|
||||
words.push_back(std::move(word));
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
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) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
|
||||
} // namespace
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
SDLock lock;
|
||||
if (!file) {
|
||||
Serial.printf("[%lu] [SCT] File not open for writing page %d\n", millis(), pageCount);
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
Serial.printf("[%lu] [SCT] Page %d processed\n", millis(), pageCount);
|
||||
|
||||
pageCount++;
|
||||
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,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight) {
|
||||
SDLock lock;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -93,14 +94,14 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
file.close();
|
||||
Serial.printf("[%lu] [SCT] Deserialization succeeded: %d pages\n", millis(), pageCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||
bool Section::clearCache() const {
|
||||
SDLock lock;
|
||||
if (!SdMan.exists(filePath.c_str())) {
|
||||
Serial.printf("[%lu] [SCT] Cache does not exist, no action needed\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -109,7 +110,6 @@ bool Section::clearCache() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Cache cleared successfully\n", millis());
|
||||
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 uint16_t viewportHeight, const std::function<void()>& progressSetupFn,
|
||||
const std::function<void(int)>& progressFn) {
|
||||
SDLock lock;
|
||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
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;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
|
||||
|
||||
// Only show progress bar for larger chapters where rendering overhead is worth it
|
||||
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
|
||||
progressSetupFn();
|
||||
@@ -217,6 +216,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
SDLock lock;
|
||||
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -224,9 +224,23 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(file, lutOffset);
|
||||
|
||||
if (lutOffset > file.size() || lutOffset < HEADER_SIZE) {
|
||||
Serial.printf("[%lu] [SCT] Invalid LUT offset %u (file size %u)\n", millis(), lutOffset, file.size());
|
||||
file.close();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
|
||||
uint32_t pagePos;
|
||||
serialization::readPod(file, pagePos);
|
||||
|
||||
if (pagePos > file.size()) {
|
||||
Serial.printf("[%lu] [SCT] Invalid page pos %u for page %d\n", millis(), pagePos, currentPage);
|
||||
file.close();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
file.seek(pagePos);
|
||||
|
||||
auto page = Page::deserialize(file);
|
||||
|
||||
@@ -16,6 +16,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
Serial.printf("[%lu] [TXB] Rendering word: %s\n", millis(), wordIt->c_str());
|
||||
renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt);
|
||||
|
||||
std::advance(wordIt, 1);
|
||||
@@ -52,6 +53,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
|
||||
// Word count
|
||||
serialization::readPod(file, wc);
|
||||
Serial.printf("[%lu] [TXB] Deserializing TextBlock: %u words\n", millis(), wc);
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large lists (max 10000 words per block)
|
||||
if (wc > 10000) {
|
||||
@@ -63,7 +65,13 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
words.resize(wc);
|
||||
wordXpos.resize(wc);
|
||||
wordStyles.resize(wc);
|
||||
for (auto& w : words) serialization::readString(file, w);
|
||||
wordStyles.resize(wc);
|
||||
int i = 0;
|
||||
for (auto& w : words) {
|
||||
if (i % 100 == 0 && i > 0) Serial.printf("[%lu] [TXB] Reading word %d/%d\n", millis(), i, wc);
|
||||
serialization::readString(file, w);
|
||||
i++;
|
||||
}
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
||||
}
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
// Serial.printf("startElement: %s\n", name);
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
// Middle of skip
|
||||
@@ -268,9 +269,17 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
|
||||
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);
|
||||
if (parser) Serial.printf("[%lu] [EHP] Parser created\n", millis());
|
||||
|
||||
int done;
|
||||
|
||||
if (!parser) {
|
||||
@@ -306,6 +315,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
}
|
||||
|
||||
const size_t len = file.read(buf, 1024);
|
||||
// Serial.printf("[%lu] [EHP] Read %d bytes\n", millis(), len);
|
||||
|
||||
if (len == 0 && file.available() > 0) {
|
||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||
@@ -331,6 +341,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
done = file.available() == 0;
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
||||
Serial.printf("[%lu] [EHP] XML_ParseBuffer returned error\n", millis());
|
||||
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
@@ -340,6 +351,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(1);
|
||||
} while (!done);
|
||||
|
||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap[fontId] = font; }
|
||||
|
||||
void GfxRenderer::clearCustomFonts(const int startId) {
|
||||
for (auto it = fontMap.lower_bound(startId); it != fontMap.end();) {
|
||||
it = fontMap.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
|
||||
switch (orientation) {
|
||||
@@ -101,10 +107,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
|
||||
// no printable characters
|
||||
if (!font.hasPrintableChars(text, style)) {
|
||||
Serial.printf("[%lu] [GFX] text '%s' has no printable chars\n", millis(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cp;
|
||||
const char* p = text;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
renderChar(font, cp, &xpos, &yPos, black, style);
|
||||
}
|
||||
@@ -164,8 +172,6 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
bool isScaled = false;
|
||||
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
||||
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
||||
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
||||
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
@@ -175,7 +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()));
|
||||
isScaled = true;
|
||||
}
|
||||
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
||||
|
||||
// Calculate output row size (2 bits per pixel, packed into bytes)
|
||||
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
||||
@@ -446,7 +451,16 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||
const EpdGlyph* glyph = fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR);
|
||||
if (!glyph) {
|
||||
// Serial.printf("[%lu] [GFX] Font %d (Regular) has no space glyph! Using fallback.\n", millis(), fontId);
|
||||
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
|
||||
if (data) {
|
||||
return data->ascender / 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return glyph->advanceX;
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
@@ -455,7 +469,13 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
|
||||
if (!data) {
|
||||
Serial.printf("[%lu] [GFX] Font %d (Regular) has no data!\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data->ascender;
|
||||
}
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
@@ -464,7 +484,13 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
||||
const EpdFontData* data = fontMap.at(fontId).getData(EpdFontFamily::REGULAR);
|
||||
if (!data) {
|
||||
Serial.printf("[%lu] [GFX] Font %d (Regular) has no data!\n", millis(), fontId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return data->advanceY;
|
||||
}
|
||||
|
||||
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
|
||||
@@ -595,7 +621,9 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
const int left = glyph->left;
|
||||
const int top = glyph->top;
|
||||
|
||||
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
||||
// Use loadGlyphBitmap to support both static and custom (SD-based) fonts
|
||||
uint8_t* buffer = nullptr; // Not used for now, as we expect a pointer or cache
|
||||
const uint8_t* bitmap = font.loadGlyphBitmap(glyph, buffer, style);
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
@@ -695,8 +723,6 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
||||
BW_BUFFER_CHUNK_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -742,7 +768,6 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
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 EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = fontFamily.getGlyph('?', style);
|
||||
}
|
||||
|
||||
// no glyph?
|
||||
if (!glyph) {
|
||||
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
||||
return;
|
||||
}
|
||||
|
||||
const int is2Bit = fontFamily.getData(style)->is2Bit;
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const EpdFont* font = fontFamily.getFont(style);
|
||||
if (!font) return;
|
||||
const EpdFontData* data = font->getData(style);
|
||||
if (!data) return;
|
||||
|
||||
const int is2Bit = data->is2Bit;
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
|
||||
const uint8_t* bitmap = nullptr;
|
||||
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
||||
const uint8_t* bitmap = font->loadGlyphBitmap(glyph, nullptr, style);
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int glyphY = 0; glyphY < height; glyphY++) {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
#include <EpdFontStyles.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "Bitmap.h"
|
||||
|
||||
@@ -46,6 +48,7 @@ class GfxRenderer {
|
||||
|
||||
// Setup
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
void clearCustomFonts(int startId = 1000);
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
@@ -72,16 +75,16 @@ class GfxRenderer {
|
||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
|
||||
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
|
||||
|
||||
// UI Components
|
||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
|
||||
@@ -90,7 +93,7 @@ class GfxRenderer {
|
||||
private:
|
||||
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
||||
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
EpdFontFamily::Style style = EpdFontStyles::REGULAR) const;
|
||||
int getTextHeight(int fontId) const;
|
||||
|
||||
public:
|
||||
|
||||
@@ -39,14 +39,26 @@ static void writeString(FsFile& file, const std::string& s) {
|
||||
static void readString(std::istream& is, std::string& s) {
|
||||
uint32_t len;
|
||||
readPod(is, len);
|
||||
if (len > 4096) {
|
||||
s = "";
|
||||
return;
|
||||
}
|
||||
s.resize(len);
|
||||
is.read(&s[0], len);
|
||||
if (len > 0) {
|
||||
is.read(&s[0], len);
|
||||
}
|
||||
}
|
||||
|
||||
static void readString(FsFile& file, std::string& s) {
|
||||
uint32_t len;
|
||||
readPod(file, len);
|
||||
if (len > 4096) {
|
||||
s = "";
|
||||
return;
|
||||
}
|
||||
s.resize(len);
|
||||
file.read(&s[0], len);
|
||||
if (len > 0) {
|
||||
file.read(&s[0], len);
|
||||
}
|
||||
}
|
||||
} // namespace serialization
|
||||
|
||||
Reference in New Issue
Block a user