Merge branch 'master' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -50,7 +50,7 @@ typedef struct {
|
||||
uint32_t compressedSize; ///< Compressed DEFLATE stream size
|
||||
uint32_t uncompressedSize; ///< Decompressed size
|
||||
uint16_t glyphCount; ///< Number of glyphs in this group
|
||||
uint16_t firstGlyphIndex; ///< First glyph index in the global glyph array
|
||||
uint32_t firstGlyphIndex; ///< First glyph index in the global glyph array
|
||||
} EpdFontGroup;
|
||||
|
||||
/// Glyph interval structure
|
||||
@@ -86,6 +86,7 @@ typedef struct {
|
||||
bool is2Bit;
|
||||
const EpdFontGroup* groups; ///< NULL for uncompressed fonts
|
||||
uint16_t groupCount; ///< 0 for uncompressed fonts
|
||||
const uint16_t* glyphToGroup; ///< Per-glyph group ID (nullptr for contiguous-group fonts)
|
||||
const EpdKernClassEntry* kernLeftClasses; ///< Sorted left-side class map (nullptr if none)
|
||||
const EpdKernClassEntry* kernRightClasses; ///< Sorted right-side class map (nullptr if none)
|
||||
const int8_t* kernMatrix; ///< Flat leftClassCount x rightClassCount matrix, 4.4 fixed-point in pixels
|
||||
|
||||
@@ -1,34 +1,55 @@
|
||||
#include "FontDecompressor.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
FontDecompressor::~FontDecompressor() { deinit(); }
|
||||
|
||||
bool FontDecompressor::init() {
|
||||
clearCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDecompressor::freeAllEntries() {
|
||||
for (auto& entry : cache) {
|
||||
if (entry.data) {
|
||||
free(entry.data);
|
||||
entry.data = nullptr;
|
||||
}
|
||||
entry.valid = false;
|
||||
}
|
||||
void FontDecompressor::deinit() {
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
|
||||
void FontDecompressor::deinit() { freeAllEntries(); }
|
||||
|
||||
void FontDecompressor::clearCache() {
|
||||
freeAllEntries();
|
||||
accessCounter = 0;
|
||||
freePageBuffer();
|
||||
freeHotGroup();
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex) {
|
||||
void FontDecompressor::freePageBuffer() {
|
||||
free(pageBuffer);
|
||||
pageBuffer = nullptr;
|
||||
free(pageGlyphs);
|
||||
pageGlyphs = nullptr;
|
||||
pageFont = nullptr;
|
||||
pageGlyphCount = 0;
|
||||
}
|
||||
|
||||
void FontDecompressor::freeHotGroup() {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
hotGlyphBuf.clear();
|
||||
hotGlyphBuf.shrink_to_fit();
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||
// O(1) path for frequency-grouped fonts with glyphToGroup mapping
|
||||
if (fontData->glyphToGroup != nullptr) {
|
||||
return fontData->glyphToGroup[glyphIndex];
|
||||
}
|
||||
|
||||
// Contiguous-group fonts: linear scan
|
||||
for (uint16_t i = 0; i < fontData->groupCount; i++) {
|
||||
uint16_t first = fontData->groups[i].firstGlyphIndex;
|
||||
uint32_t first = fontData->groups[i].firstGlyphIndex;
|
||||
if (glyphIndex >= first && glyphIndex < first + fontData->groups[i].glyphCount) {
|
||||
return i;
|
||||
}
|
||||
@@ -36,99 +57,412 @@ uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint16_t g
|
||||
return fontData->groupCount; // sentinel = not found
|
||||
}
|
||||
|
||||
FontDecompressor::CacheEntry* FontDecompressor::findInCache(const EpdFontData* fontData, uint16_t groupIndex) {
|
||||
for (auto& entry : cache) {
|
||||
if (entry.valid && entry.font == fontData && entry.groupIndex == groupIndex) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FontDecompressor::CacheEntry* FontDecompressor::findEvictionCandidate() {
|
||||
// Find an invalid slot first
|
||||
for (auto& entry : cache) {
|
||||
if (!entry.valid) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
// Otherwise evict LRU
|
||||
CacheEntry* lru = &cache[0];
|
||||
for (auto& entry : cache) {
|
||||
if (entry.lastUsed < lru->lastUsed) {
|
||||
lru = &entry;
|
||||
}
|
||||
}
|
||||
return lru;
|
||||
}
|
||||
|
||||
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry) {
|
||||
bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf,
|
||||
uint32_t outSize) {
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
// Free old buffer if reusing a slot
|
||||
if (entry->data) {
|
||||
free(entry->data);
|
||||
entry->data = nullptr;
|
||||
}
|
||||
entry->valid = false;
|
||||
|
||||
// Allocate output buffer
|
||||
auto* outBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
|
||||
if (!outBuf) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for group %u", group.uncompressedSize, groupIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t tDecomp = millis();
|
||||
inflateReader.init(false);
|
||||
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
|
||||
if (!inflateReader.read(outBuf, group.uncompressedSize)) {
|
||||
if (!inflateReader.read(outBuf, outSize)) {
|
||||
stats.decompressTimeMs += millis() - tDecomp;
|
||||
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
|
||||
free(outBuf);
|
||||
return false;
|
||||
}
|
||||
|
||||
entry->font = fontData;
|
||||
entry->groupIndex = groupIndex;
|
||||
entry->data = outBuf;
|
||||
entry->dataSize = group.uncompressedSize;
|
||||
entry->valid = true;
|
||||
stats.decompressTimeMs += millis() - tDecomp;
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex) {
|
||||
// --- Byte-aligned helpers ---
|
||||
|
||||
uint32_t FontDecompressor::getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex) {
|
||||
uint32_t offset = 0;
|
||||
|
||||
auto accumGlyph = [&](const EpdGlyph& g) {
|
||||
if (g.width > 0 && g.height > 0) {
|
||||
offset += ((g.width + 3) / 4) * g.height;
|
||||
}
|
||||
};
|
||||
|
||||
if (fontData->glyphToGroup) {
|
||||
// Frequency-grouped: scan glyphs before glyphIndex that belong to this group
|
||||
for (uint32_t i = 0; i < glyphIndex; i++) {
|
||||
if (fontData->glyphToGroup[i] == groupIndex) {
|
||||
accumGlyph(fontData->glyph[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Contiguous-group: sum aligned sizes of preceding glyphs in the group
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
for (uint32_t i = group.firstGlyphIndex; i < glyphIndex; i++) {
|
||||
accumGlyph(fontData->glyph[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
void FontDecompressor::compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width,
|
||||
uint8_t height) {
|
||||
if (width == 0 || height == 0) return;
|
||||
const uint32_t rowStride = (width + 3) / 4;
|
||||
if (width % 4 == 0) {
|
||||
memcpy(packedDst, alignedSrc, rowStride * height);
|
||||
return;
|
||||
}
|
||||
uint8_t outByte = 0, outBits = 0;
|
||||
uint32_t writeIdx = 0;
|
||||
for (uint8_t y = 0; y < height; y++) {
|
||||
for (uint8_t x = 0; x < width; x++) {
|
||||
outByte = (outByte << 2) | ((alignedSrc[y * rowStride + x / 4] >> ((3 - (x % 4)) * 2)) & 0x3);
|
||||
outBits += 2;
|
||||
if (outBits == 8) {
|
||||
packedDst[writeIdx++] = outByte;
|
||||
outByte = 0;
|
||||
outBits = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (outBits > 0) packedDst[writeIdx] = outByte << (8 - outBits);
|
||||
}
|
||||
|
||||
// --- getBitmap: page buffer → hot group → decompress ---
|
||||
|
||||
const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex) {
|
||||
const uint32_t tStart = micros();
|
||||
stats.getBitmapCalls++;
|
||||
|
||||
if (!fontData->groups || fontData->groupCount == 0) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
// Check page buffer first (populated by prewarmCache)
|
||||
if (pageBuffer && pageFont == fontData && pageGlyphCount > 0) {
|
||||
int left = 0, right = pageGlyphCount - 1;
|
||||
while (left <= right) {
|
||||
int mid = left + (right - left) / 2;
|
||||
if (pageGlyphs[mid].glyphIndex == glyphIndex) {
|
||||
if (pageGlyphs[mid].bufferOffset != UINT32_MAX) {
|
||||
stats.cacheHits++;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return &pageBuffer[pageGlyphs[mid].bufferOffset];
|
||||
}
|
||||
break; // Not extracted during prewarm; fall through to hot-group path
|
||||
}
|
||||
if (pageGlyphs[mid].glyphIndex < glyphIndex)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: hot group slot
|
||||
uint16_t groupIndex = getGroupIndex(fontData, glyphIndex);
|
||||
if (groupIndex >= fontData->groupCount) {
|
||||
LOG_ERR("FDC", "Glyph %u not found in any group", glyphIndex);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check cache
|
||||
CacheEntry* entry = findInCache(fontData, groupIndex);
|
||||
if (entry) {
|
||||
entry->lastUsed = ++accessCounter;
|
||||
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
|
||||
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
|
||||
glyph->dataLength, groupIndex, entry->dataSize);
|
||||
// Check if hot group already has this group decompressed — if not, decompress it
|
||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
hotGroup.resize(group.uncompressedSize);
|
||||
if (hotGroup.empty()) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
return &entry->data[glyph->dataOffset];
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
hotGroupFont = fontData;
|
||||
hotGroupIndex = groupIndex;
|
||||
stats.hotGroupBytes = group.uncompressedSize;
|
||||
} else {
|
||||
stats.cacheHits++;
|
||||
}
|
||||
|
||||
// Cache miss - decompress
|
||||
entry = findEvictionCandidate();
|
||||
if (!decompressGroup(fontData, groupIndex, entry)) {
|
||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
||||
hotGlyphBuf.resize(glyph->dataLength);
|
||||
}
|
||||
if (hotGlyphBuf.empty()) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
entry->lastUsed = ++accessCounter;
|
||||
if (glyph->dataOffset + glyph->dataLength > entry->dataSize) {
|
||||
LOG_ERR("FDC", "dataOffset %u + dataLength %u out of bounds for group %u (size %u)", glyph->dataOffset,
|
||||
glyph->dataLength, groupIndex, entry->dataSize);
|
||||
return nullptr;
|
||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return hotGlyphBuf.data();
|
||||
}
|
||||
|
||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||
|
||||
int32_t FontDecompressor::findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint) {
|
||||
const EpdUnicodeInterval* intervals = fontData->intervals;
|
||||
const int count = fontData->intervalCount;
|
||||
|
||||
if (count == 0) return -1;
|
||||
|
||||
// Binary search
|
||||
int left = 0;
|
||||
int right = count - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
const EpdUnicodeInterval* interval = &intervals[mid];
|
||||
|
||||
if (codepoint < interval->first) {
|
||||
right = mid - 1;
|
||||
} else if (codepoint > interval->last) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
return static_cast<int32_t>(interval->offset + (codepoint - interval->first));
|
||||
}
|
||||
}
|
||||
return &entry->data[glyph->dataOffset];
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8Text) {
|
||||
freePageBuffer();
|
||||
if (!fontData || !fontData->groups || !utf8Text) return 0;
|
||||
|
||||
// Step 1: Collect unique glyph indices needed for this page
|
||||
uint32_t neededGlyphs[MAX_PAGE_GLYPHS];
|
||||
uint16_t glyphCount = 0;
|
||||
bool glyphCapWarned = false;
|
||||
|
||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(utf8Text);
|
||||
while (*p) {
|
||||
uint32_t cp = utf8NextCodepoint(&p);
|
||||
if (cp == 0) break;
|
||||
|
||||
int32_t glyphIdx = findGlyphIndex(fontData, cp);
|
||||
if (glyphIdx < 0) continue;
|
||||
|
||||
// Deduplicate
|
||||
bool found = false;
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
if (neededGlyphs[i] == static_cast<uint32_t>(glyphIdx)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (glyphCount < MAX_PAGE_GLYPHS) {
|
||||
neededGlyphs[glyphCount++] = static_cast<uint32_t>(glyphIdx);
|
||||
} else if (!glyphCapWarned) {
|
||||
LOG_DBG("FDC", "Glyph cap (%u) reached during prewarm; excess glyphs will use hot-group fallback",
|
||||
MAX_PAGE_GLYPHS);
|
||||
glyphCapWarned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (glyphCount == 0) return 0;
|
||||
|
||||
// Step 2: Compute total buffer size and collect unique groups
|
||||
uint32_t totalBytes = 0;
|
||||
uint16_t neededGroups[128];
|
||||
uint8_t groupCount = 0;
|
||||
bool groupCapWarned = false;
|
||||
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
totalBytes += fontData->glyph[neededGlyphs[i]].dataLength;
|
||||
uint16_t gi = getGroupIndex(fontData, neededGlyphs[i]);
|
||||
bool found = false;
|
||||
for (uint8_t j = 0; j < groupCount; j++) {
|
||||
if (neededGroups[j] == gi) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
if (groupCount < 128) {
|
||||
neededGroups[groupCount++] = gi;
|
||||
} else if (!groupCapWarned) {
|
||||
LOG_DBG("FDC", "Group cap (128) reached during prewarm; some groups will use hot-group fallback");
|
||||
groupCapWarned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats.uniqueGroupsAccessed = groupCount;
|
||||
|
||||
// Step 3: Allocate page buffer and lookup table
|
||||
pageBuffer = static_cast<uint8_t*>(malloc(totalBytes));
|
||||
pageGlyphs = static_cast<PageGlyphEntry*>(malloc(glyphCount * sizeof(PageGlyphEntry)));
|
||||
if (!pageBuffer || !pageGlyphs) {
|
||||
LOG_ERR("FDC", "Failed to allocate page buffer (%u bytes, %u glyphs)", totalBytes, glyphCount);
|
||||
freePageBuffer();
|
||||
return glyphCount;
|
||||
}
|
||||
stats.pageBufferBytes = totalBytes;
|
||||
stats.pageGlyphsBytes = glyphCount * sizeof(PageGlyphEntry);
|
||||
|
||||
pageFont = fontData;
|
||||
pageGlyphCount = glyphCount;
|
||||
|
||||
// Initialize lookup entries (bufferOffset = UINT32_MAX means not yet extracted)
|
||||
for (uint16_t i = 0; i < glyphCount; i++) {
|
||||
pageGlyphs[i] = {neededGlyphs[i], UINT32_MAX, 0};
|
||||
}
|
||||
|
||||
// Sort by glyphIndex for binary search in getBitmap()
|
||||
for (uint16_t i = 1; i < glyphCount; i++) {
|
||||
PageGlyphEntry key = pageGlyphs[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && pageGlyphs[j].glyphIndex > key.glyphIndex) {
|
||||
pageGlyphs[j + 1] = pageGlyphs[j];
|
||||
j--;
|
||||
}
|
||||
pageGlyphs[j + 1] = key;
|
||||
}
|
||||
|
||||
// Step 3b: Pre-scan to compute each needed glyph's byte-aligned offset within its group.
|
||||
// This avoids recomputing aligned offsets per group during extraction in step 4.
|
||||
uint32_t groupAlignedTracker[128] = {}; // running byte-aligned offset for each needed group
|
||||
|
||||
if (fontData->glyphToGroup) {
|
||||
// Frequency-grouped: single O(totalGlyphs) pass through glyphToGroup
|
||||
const auto& lastInterval = fontData->intervals[fontData->intervalCount - 1];
|
||||
const uint32_t totalGlyphs = lastInterval.offset + (lastInterval.last - lastInterval.first + 1);
|
||||
|
||||
for (uint32_t i = 0; i < totalGlyphs; i++) {
|
||||
const uint16_t gi = fontData->glyphToGroup[i];
|
||||
// Find this glyph's group position in neededGroups
|
||||
uint8_t gpPos = groupCount;
|
||||
for (uint8_t j = 0; j < groupCount; j++) {
|
||||
if (neededGroups[j] == gi) {
|
||||
gpPos = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gpPos == groupCount) continue; // not a needed group
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[i];
|
||||
|
||||
// Binary search in sorted pageGlyphs to find if glyph i is needed
|
||||
int left = 0, right = (int)pageGlyphCount - 1;
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
if (pageGlyphs[mid].glyphIndex == i) {
|
||||
pageGlyphs[mid].alignedOffset = groupAlignedTracker[gpPos];
|
||||
break;
|
||||
}
|
||||
if (pageGlyphs[mid].glyphIndex < i)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
|
||||
if (glyph.width > 0 && glyph.height > 0) {
|
||||
groupAlignedTracker[gpPos] += ((glyph.width + 3) / 4) * glyph.height;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Contiguous-group: iterate each needed group's glyphs directly
|
||||
for (uint8_t g = 0; g < groupCount; g++) {
|
||||
const EpdFontGroup& group = fontData->groups[neededGroups[g]];
|
||||
uint32_t alignedOff = 0;
|
||||
for (uint16_t j = 0; j < group.glyphCount; j++) {
|
||||
const uint32_t glyphI = group.firstGlyphIndex + j;
|
||||
const EpdGlyph& glyph = fontData->glyph[glyphI];
|
||||
|
||||
int left = 0, right = (int)pageGlyphCount - 1;
|
||||
while (left <= right) {
|
||||
const int mid = left + (right - left) / 2;
|
||||
if (pageGlyphs[mid].glyphIndex == glyphI) {
|
||||
pageGlyphs[mid].alignedOffset = alignedOff;
|
||||
break;
|
||||
}
|
||||
if (pageGlyphs[mid].glyphIndex < glyphI)
|
||||
left = mid + 1;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
|
||||
if (glyph.width > 0 && glyph.height > 0) {
|
||||
alignedOff += ((glyph.width + 3) / 4) * glyph.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: For each unique group, decompress to temp buffer and extract needed glyphs
|
||||
uint32_t writeOffset = 0;
|
||||
int missed = 0;
|
||||
|
||||
for (uint8_t g = 0; g < groupCount; g++) {
|
||||
uint16_t groupIdx = neededGroups[g];
|
||||
const EpdFontGroup& group = fontData->groups[groupIdx];
|
||||
|
||||
auto* tempBuf = static_cast<uint8_t*>(malloc(group.uncompressedSize));
|
||||
if (!tempBuf) {
|
||||
LOG_ERR("FDC", "Failed to allocate temp buffer (%u bytes) for group %u", group.uncompressedSize, groupIdx);
|
||||
missed++;
|
||||
continue;
|
||||
}
|
||||
if (group.uncompressedSize > stats.peakTempBytes) {
|
||||
stats.peakTempBytes = group.uncompressedSize;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIdx, tempBuf, group.uncompressedSize)) {
|
||||
free(tempBuf);
|
||||
missed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract needed glyphs directly from the byte-aligned temp buffer, compacting on the fly.
|
||||
// alignedOffset was pre-computed in step 3b — no full-group compact scan needed.
|
||||
for (uint16_t i = 0; i < pageGlyphCount; i++) {
|
||||
if (pageGlyphs[i].bufferOffset != UINT32_MAX) continue; // already extracted
|
||||
if (getGroupIndex(fontData, pageGlyphs[i].glyphIndex) != groupIdx) continue;
|
||||
|
||||
const EpdGlyph& glyph = fontData->glyph[pageGlyphs[i].glyphIndex];
|
||||
compactSingleGlyph(&tempBuf[pageGlyphs[i].alignedOffset], &pageBuffer[writeOffset], glyph.width, glyph.height);
|
||||
pageGlyphs[i].bufferOffset = writeOffset;
|
||||
writeOffset += glyph.dataLength;
|
||||
}
|
||||
|
||||
free(tempBuf);
|
||||
}
|
||||
|
||||
LOG_DBG("FDC", "Prewarm: %u glyphs in %u bytes from %u groups (%d missed)", glyphCount, writeOffset, groupCount,
|
||||
missed);
|
||||
|
||||
return missed;
|
||||
}
|
||||
|
||||
// --- Stats ---
|
||||
|
||||
void FontDecompressor::resetStats() { stats = Stats{}; }
|
||||
|
||||
void FontDecompressor::logStats(const char* label) {
|
||||
const uint32_t total = stats.cacheHits + stats.cacheMisses;
|
||||
LOG_DBG("FDC", "[%s] hits=%lu misses=%lu (%.1f%% hit rate)", label, stats.cacheHits, stats.cacheMisses,
|
||||
total > 0 ? 100.0f * stats.cacheHits / total : 0.0f);
|
||||
LOG_DBG("FDC", "[%s] decompress=%lums groups_accessed=%u", label, stats.decompressTimeMs, stats.uniqueGroupsAccessed);
|
||||
LOG_DBG("FDC", "[%s] mem: pageBuf=%lu pageGlyphs=%lu hotGroup=%lu peakTemp=%lu", label, stats.pageBufferBytes,
|
||||
stats.pageGlyphsBytes, stats.hotGroupBytes, stats.peakTempBytes);
|
||||
if (stats.getBitmapCalls > 0) {
|
||||
LOG_DBG("FDC", "[%s] getBitmap: %lu calls, %luus total, %luus/call avg", label, stats.getBitmapCalls,
|
||||
stats.getBitmapTimeUs, stats.getBitmapTimeUs / stats.getBitmapCalls);
|
||||
}
|
||||
resetStats();
|
||||
}
|
||||
|
||||
@@ -2,39 +2,78 @@
|
||||
|
||||
#include <InflateReader.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
public:
|
||||
static constexpr uint16_t MAX_PAGE_GLYPHS = 512;
|
||||
|
||||
FontDecompressor() = default;
|
||||
~FontDecompressor();
|
||||
|
||||
bool init();
|
||||
void deinit();
|
||||
|
||||
// Returns pointer to decompressed bitmap data for the given glyph.
|
||||
// Valid until LRU eviction (safe for the duration of one glyph render).
|
||||
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint16_t glyphIndex);
|
||||
// Checks the page buffer (from prewarm) first, then falls back to the hot group slot.
|
||||
const uint8_t* getBitmap(const EpdFontData* fontData, const EpdGlyph* glyph, uint32_t glyphIndex);
|
||||
|
||||
// Evict all cached decompressed groups (call between pages for within-page-only caching).
|
||||
// Free all cached data (page buffer + hot group).
|
||||
void clearCache();
|
||||
|
||||
private:
|
||||
static constexpr uint8_t CACHE_SLOTS = 4;
|
||||
// Pre-scan UTF-8 text and extract needed glyph bitmaps into a flat page buffer.
|
||||
// Each group is decompressed once into a temp buffer; only needed glyphs are kept.
|
||||
// Returns the number of glyphs that couldn't be loaded (0 on full success).
|
||||
int prewarmCache(const EpdFontData* fontData, const char* utf8Text);
|
||||
|
||||
struct CacheEntry {
|
||||
const EpdFontData* font = nullptr;
|
||||
uint16_t groupIndex = 0;
|
||||
uint8_t* data = nullptr;
|
||||
uint32_t dataSize = 0;
|
||||
uint32_t lastUsed = 0;
|
||||
bool valid = false;
|
||||
struct Stats {
|
||||
uint32_t cacheHits = 0;
|
||||
uint32_t cacheMisses = 0;
|
||||
uint32_t decompressTimeMs = 0;
|
||||
uint16_t uniqueGroupsAccessed = 0;
|
||||
uint32_t pageBufferBytes = 0; // pageBuffer allocation
|
||||
uint32_t pageGlyphsBytes = 0; // pageGlyphs lookup table allocation
|
||||
uint32_t hotGroupBytes = 0; // current hot group allocation
|
||||
uint32_t peakTempBytes = 0; // largest temp buffer in prewarm
|
||||
uint32_t getBitmapTimeUs = 0; // cumulative getBitmap time (micros)
|
||||
uint32_t getBitmapCalls = 0; // number of getBitmap calls
|
||||
};
|
||||
void logStats(const char* label = "FDC");
|
||||
void resetStats();
|
||||
const Stats& getStats() const { return stats; }
|
||||
|
||||
private:
|
||||
Stats stats;
|
||||
InflateReader inflateReader;
|
||||
CacheEntry cache[CACHE_SLOTS] = {};
|
||||
uint32_t accessCounter = 0;
|
||||
|
||||
void freeAllEntries();
|
||||
uint16_t getGroupIndex(const EpdFontData* fontData, uint16_t glyphIndex);
|
||||
CacheEntry* findInCache(const EpdFontData* fontData, uint16_t groupIndex);
|
||||
CacheEntry* findEvictionCandidate();
|
||||
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, CacheEntry* entry);
|
||||
// Page buffer: flat array of prewarmed glyph bitmaps with sorted lookup
|
||||
struct PageGlyphEntry {
|
||||
uint32_t glyphIndex;
|
||||
uint32_t bufferOffset;
|
||||
uint32_t alignedOffset; // byte-aligned offset within its decompressed group (set during prewarm pre-scan)
|
||||
};
|
||||
uint8_t* pageBuffer = nullptr;
|
||||
const EpdFontData* pageFont = nullptr;
|
||||
PageGlyphEntry* pageGlyphs = nullptr;
|
||||
uint16_t pageGlyphCount = 0;
|
||||
|
||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||
const EpdFontData* hotGroupFont = nullptr;
|
||||
uint16_t hotGroupIndex = UINT16_MAX;
|
||||
std::vector<uint8_t> hotGroup;
|
||||
|
||||
// Scratch buffer for compacting a single glyph from the hot group.
|
||||
// Valid until the next getBitmap() call.
|
||||
std::vector<uint8_t> hotGlyphBuf;
|
||||
|
||||
void freePageBuffer();
|
||||
void freeHotGroup();
|
||||
uint16_t getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex);
|
||||
uint32_t getAlignedOffset(const EpdFontData* fontData, uint16_t groupIndex, uint32_t glyphIndex);
|
||||
bool decompressGroup(const EpdFontData* fontData, uint16_t groupIndex, uint8_t* outBuf, uint32_t outSize);
|
||||
static void compactSingleGlyph(const uint8_t* alignedSrc, uint8_t* packedDst, uint8_t width, uint8_t height);
|
||||
static int32_t findGlyphIndex(const EpdFontData* fontData, uint32_t codepoint);
|
||||
};
|
||||
|
||||
+1693
-1972
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2017
-2445
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2360
-2652
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2813
-3359
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1676
-1977
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2042
-2448
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2387
-2734
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2792
-3185
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3118,6 +3118,7 @@ static const EpdFontData notosans_8_regular = {
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
notosans_8_regularKernLeftClasses,
|
||||
notosans_8_regularKernRightClasses,
|
||||
notosans_8_regularKernMatrix,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3092,6 +3092,7 @@ static const EpdFontData ubuntu_10_bold = {
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
ubuntu_10_boldKernLeftClasses,
|
||||
ubuntu_10_boldKernRightClasses,
|
||||
ubuntu_10_boldKernMatrix,
|
||||
|
||||
@@ -2768,6 +2768,7 @@ static const EpdFontData ubuntu_10_regular = {
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
ubuntu_10_regularKernLeftClasses,
|
||||
ubuntu_10_regularKernRightClasses,
|
||||
ubuntu_10_regularKernMatrix,
|
||||
|
||||
@@ -3478,6 +3478,7 @@ static const EpdFontData ubuntu_12_bold = {
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
ubuntu_12_boldKernLeftClasses,
|
||||
ubuntu_12_boldKernRightClasses,
|
||||
ubuntu_12_boldKernMatrix,
|
||||
|
||||
@@ -3129,6 +3129,7 @@ static const EpdFontData ubuntu_12_regular = {
|
||||
false,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
ubuntu_12_regularKernLeftClasses,
|
||||
ubuntu_12_regularKernRightClasses,
|
||||
ubuntu_12_regularKernMatrix,
|
||||
|
||||
@@ -689,7 +689,38 @@ print(f"ligatures: {len(ligature_pairs)} pairs extracted", file=sys.stderr)
|
||||
|
||||
compress = args.compress
|
||||
|
||||
|
||||
def to_byte_aligned(packed, width, height):
|
||||
"""Convert packed 2-bit bitmap to byte-aligned format (rows padded to byte boundary).
|
||||
|
||||
In packed format, pixels flow continuously across row boundaries (4 pixels/byte).
|
||||
In byte-aligned format, each row starts at a byte boundary, padding the last byte
|
||||
of each row with zero bits if width % 4 != 0. This improves DEFLATE compression
|
||||
because identical pixel rows produce identical byte patterns regardless of position.
|
||||
"""
|
||||
if width == 0 or height == 0:
|
||||
return b''
|
||||
row_stride = (width + 3) // 4 # bytes per byte-aligned row
|
||||
aligned = bytearray(row_stride * height)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
# Read pixel from packed format (continuous bit stream)
|
||||
packed_pos = y * width + x
|
||||
packed_byte_idx = packed_pos // 4
|
||||
packed_shift = (3 - (packed_pos % 4)) * 2
|
||||
pixel = (packed[packed_byte_idx] >> packed_shift) & 0x3
|
||||
|
||||
# Write pixel to byte-aligned format (row-aligned)
|
||||
aligned_byte_idx = y * row_stride + x // 4
|
||||
aligned_shift = (3 - (x % 4)) * 2
|
||||
aligned[aligned_byte_idx] |= (pixel << aligned_shift)
|
||||
return bytes(aligned)
|
||||
|
||||
|
||||
# Build groups for compression
|
||||
if compress and not is2Bit:
|
||||
print("Error: --compress requires --2bit (byte-aligned compression only supports 2-bit format)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if compress:
|
||||
# Script-based grouping: glyphs that co-occur in typical text rendering
|
||||
# are grouped together for efficient LRU caching on the embedded target.
|
||||
@@ -747,11 +778,12 @@ if compress:
|
||||
|
||||
for first_idx, count in groups:
|
||||
# Concatenate bitmap data for this group
|
||||
group_data = b''
|
||||
packed_len = 0
|
||||
group_aligned = bytearray()
|
||||
for gi in range(first_idx, first_idx + count):
|
||||
props, packed = all_glyphs[gi]
|
||||
# Update glyph's dataOffset to be within-group offset
|
||||
within_group_offset = len(group_data)
|
||||
# Update glyph's dataOffset to be within-group offset (packed offset)
|
||||
within_group_offset = packed_len
|
||||
old_props = modified_glyph_props[gi]
|
||||
modified_glyph_props[gi] = GlyphProps(
|
||||
width=old_props.width,
|
||||
@@ -763,13 +795,14 @@ if compress:
|
||||
data_offset=within_group_offset,
|
||||
code_point=old_props.code_point,
|
||||
)
|
||||
group_data += packed
|
||||
packed_len += len(packed)
|
||||
group_aligned.extend(to_byte_aligned(packed, old_props.width, old_props.height))
|
||||
|
||||
# Compress with raw DEFLATE (no zlib/gzip header)
|
||||
# Compress byte-aligned data with raw DEFLATE (no zlib/gzip header)
|
||||
compressor = zlib.compressobj(level=9, wbits=-15)
|
||||
compressed = compressor.compress(group_data) + compressor.flush()
|
||||
compressed = compressor.compress(bytes(group_aligned)) + compressor.flush()
|
||||
|
||||
compressed_groups.append((compressed, len(group_data), count, first_idx))
|
||||
compressed_groups.append((compressed, len(group_aligned), count, first_idx))
|
||||
compressed_bitmap_data.extend(compressed)
|
||||
compressed_offset += len(compressed)
|
||||
|
||||
@@ -862,8 +895,10 @@ if compress:
|
||||
print(f" {font_name}Groups,")
|
||||
print(f" {len(compressed_groups)},")
|
||||
else:
|
||||
print(f" nullptr,")
|
||||
print(f" 0,")
|
||||
print(" nullptr,")
|
||||
print(" 0,")
|
||||
# glyphToGroup (not used for script-grouped fonts)
|
||||
print(" nullptr,")
|
||||
if kern_map:
|
||||
print(f" {font_name}KernLeftClasses,")
|
||||
print(f" {font_name}KernRightClasses,")
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
Round-trip verification for compressed font headers.
|
||||
|
||||
Parses each generated .h file in the given directory, identifies compressed fonts
|
||||
(those with a Groups array), decompresses each group, and verifies that
|
||||
decompression succeeds and all glyph offsets/lengths fall within bounds.
|
||||
(those with a Groups array), decompresses each group (byte-aligned bitmap format),
|
||||
compacts to packed format, and verifies the data matches expected glyph sizes.
|
||||
|
||||
Supports both contiguous-group fonts (Latin) and frequency-grouped fonts (CJK)
|
||||
with glyphToGroup mapping arrays.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -18,6 +22,11 @@ def parse_hex_array(text):
|
||||
return bytes(int(h, 16) for h in hex_vals)
|
||||
|
||||
|
||||
def parse_uint8_array(text):
|
||||
"""Extract uint8/uint16 values from a C array string like '{ 0, 1, 0xFF, ... }'"""
|
||||
return [int(v, 0) for v in re.findall(r'\b0x[0-9A-Fa-f]+\b|\b\d+\b', text)]
|
||||
|
||||
|
||||
def parse_groups(text):
|
||||
"""Parse EpdFontGroup array entries: { compressedOffset, compressedSize, uncompressedSize, glyphCount, firstGlyphIndex }"""
|
||||
groups = []
|
||||
@@ -48,6 +57,45 @@ def parse_glyphs(text):
|
||||
return glyphs
|
||||
|
||||
|
||||
def get_group_glyph_indices(group, group_index, glyphs, glyph_to_group):
|
||||
"""Get the ordered list of glyph indices belonging to a group."""
|
||||
if glyph_to_group is not None:
|
||||
# Frequency-grouped: scan all glyphs
|
||||
return [i for i in range(len(glyphs)) if glyph_to_group[i] == group_index]
|
||||
else:
|
||||
# Contiguous: sequential from firstGlyphIndex
|
||||
first = group['firstGlyphIndex']
|
||||
return list(range(first, first + group['glyphCount']))
|
||||
|
||||
|
||||
def compact_aligned_to_packed(aligned_data, width, height):
|
||||
"""Convert byte-aligned 2-bit bitmap to packed format (reverse of to_byte_aligned).
|
||||
|
||||
In byte-aligned format, each row starts at a byte boundary.
|
||||
In packed format, pixels flow continuously across row boundaries (4 pixels/byte).
|
||||
"""
|
||||
if width == 0 or height == 0:
|
||||
return b''
|
||||
packed_size = math.ceil(width * height / 4)
|
||||
packed = bytearray(packed_size)
|
||||
row_stride = (width + 3) // 4 # bytes per byte-aligned row
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
# Read pixel from byte-aligned format (row-aligned)
|
||||
aligned_byte_idx = y * row_stride + x // 4
|
||||
aligned_shift = (3 - (x % 4)) * 2
|
||||
pixel = (aligned_data[aligned_byte_idx] >> aligned_shift) & 0x3
|
||||
|
||||
# Write pixel to packed format (continuous bit stream)
|
||||
packed_pos = y * width + x
|
||||
packed_byte_idx = packed_pos // 4
|
||||
packed_shift = (3 - (packed_pos % 4)) * 2
|
||||
packed[packed_byte_idx] |= (pixel << packed_shift)
|
||||
|
||||
return bytes(packed)
|
||||
|
||||
|
||||
def verify_font_file(filepath):
|
||||
"""Verify a single font header file. Returns (font_name, success, message)."""
|
||||
with open(filepath, 'r') as f:
|
||||
@@ -92,6 +140,20 @@ def verify_font_file(filepath):
|
||||
|
||||
glyphs = parse_glyphs(glyphs_match.group(1))
|
||||
|
||||
# Check for glyphToGroup array (frequency-grouped fonts)
|
||||
glyph_to_group = None
|
||||
g2g_match = re.search(
|
||||
r'static const uint16_t ' + re.escape(font_name) + r'GlyphToGroup\[\]\s*=\s*\{(.+?)\};',
|
||||
content, re.DOTALL
|
||||
)
|
||||
if g2g_match:
|
||||
glyph_to_group = parse_uint8_array(g2g_match.group(1))
|
||||
if len(glyph_to_group) != len(glyphs):
|
||||
return (font_name, False, f"glyphToGroup length ({len(glyph_to_group)}) != glyph count ({len(glyphs)})")
|
||||
max_group_id = max(glyph_to_group)
|
||||
if max_group_id >= len(groups):
|
||||
return (font_name, False, f"glyphToGroup contains group ID {max_group_id} but only {len(groups)} groups exist")
|
||||
|
||||
# Verify each group
|
||||
for gi, group in enumerate(groups):
|
||||
# Extract compressed chunk
|
||||
@@ -99,7 +161,7 @@ def verify_font_file(filepath):
|
||||
if len(chunk) != group['compressedSize']:
|
||||
return (font_name, False, f"group {gi}: compressed data truncated (expected {group['compressedSize']}, got {len(chunk)})")
|
||||
|
||||
# Decompress with raw DEFLATE
|
||||
# Decompress with raw DEFLATE — result is byte-aligned data
|
||||
try:
|
||||
decompressed = zlib.decompress(chunk, -15)
|
||||
except zlib.error as e:
|
||||
@@ -108,22 +170,64 @@ def verify_font_file(filepath):
|
||||
if len(decompressed) != group['uncompressedSize']:
|
||||
return (font_name, False, f"group {gi}: size mismatch (expected {group['uncompressedSize']}, got {len(decompressed)})")
|
||||
|
||||
# Verify each glyph's data within the group
|
||||
first = group['firstGlyphIndex']
|
||||
for j in range(group['glyphCount']):
|
||||
glyph_idx = first + j
|
||||
# Get glyph indices for this group
|
||||
group_glyph_indices = get_group_glyph_indices(group, gi, glyphs, glyph_to_group)
|
||||
if glyph_to_group is not None and len(group_glyph_indices) != group['glyphCount']:
|
||||
return (font_name, False,
|
||||
f"group {gi}: glyphCount {group['glyphCount']} != mapping count {len(group_glyph_indices)}")
|
||||
|
||||
# Walk through byte-aligned data, compact each glyph, and verify against packed format
|
||||
byte_aligned_offset = 0
|
||||
packed_offset = 0
|
||||
|
||||
for glyph_idx in group_glyph_indices:
|
||||
if glyph_idx >= len(glyphs):
|
||||
return (font_name, False, f"group {gi}: glyph index {glyph_idx} out of range")
|
||||
|
||||
glyph = glyphs[glyph_idx]
|
||||
offset = glyph['dataOffset']
|
||||
length = glyph['dataLength']
|
||||
width = glyph['width']
|
||||
height = glyph['height']
|
||||
|
||||
if offset + length > len(decompressed):
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: data extends beyond decompressed buffer "
|
||||
f"(offset={offset}, length={length}, decompressed_size={len(decompressed)})")
|
||||
if width == 0 or height == 0:
|
||||
# Zero-size glyphs should have dataOffset == current packed_offset and dataLength == 0
|
||||
if glyph['dataOffset'] != packed_offset:
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: zero-size glyph dataOffset {glyph['dataOffset']} != expected packed offset {packed_offset}")
|
||||
if glyph['dataLength'] != 0:
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: zero-size glyph dataLength {glyph['dataLength']} != expected 0")
|
||||
continue
|
||||
|
||||
return (font_name, True, f"{len(groups)} groups, {len(glyphs)} glyphs OK")
|
||||
aligned_size = ((width + 3) // 4) * height
|
||||
packed_size = math.ceil(width * height / 4)
|
||||
|
||||
# Verify packed offset and size match glyph metadata
|
||||
if glyph['dataOffset'] != packed_offset:
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: dataOffset {glyph['dataOffset']} != expected packed offset {packed_offset}")
|
||||
if glyph['dataLength'] != packed_size:
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: dataLength {glyph['dataLength']} != expected packed length {packed_size} "
|
||||
f"(width={width}, height={height})")
|
||||
|
||||
# Extract byte-aligned data for this glyph
|
||||
if byte_aligned_offset + aligned_size > len(decompressed):
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: byte-aligned data extends beyond decompressed buffer "
|
||||
f"(offset={byte_aligned_offset}, size={aligned_size}, buf_size={len(decompressed)})")
|
||||
|
||||
aligned_glyph = decompressed[byte_aligned_offset:byte_aligned_offset + aligned_size]
|
||||
|
||||
# Compact to packed and verify pixel values are valid (0-3 for 2-bit)
|
||||
packed_glyph = compact_aligned_to_packed(aligned_glyph, width, height)
|
||||
if len(packed_glyph) != packed_size:
|
||||
return (font_name, False, f"group {gi}, glyph {glyph_idx}: compacted size {len(packed_glyph)} != expected {packed_size}")
|
||||
|
||||
byte_aligned_offset += aligned_size
|
||||
packed_offset += packed_size
|
||||
|
||||
# Verify total byte-aligned size matches uncompressedSize
|
||||
if byte_aligned_offset != group['uncompressedSize']:
|
||||
return (font_name, False, f"group {gi}: total byte-aligned size {byte_aligned_offset} != uncompressedSize {group['uncompressedSize']}")
|
||||
|
||||
extra_info = ""
|
||||
if glyph_to_group is not None:
|
||||
extra_info = " (frequency-grouped)"
|
||||
return (font_name, True, f"{len(groups)} groups, {len(glyphs)} glyphs OK{extra_info}")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -101,20 +101,19 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
applyParagraphIndent();
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId, EpdFontFamily::REGULAR);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
if (hyphenationEnabled) {
|
||||
// Use greedy layout that can split words mid-loop when a hyphenated prefix fits.
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
} else {
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, wordContinues);
|
||||
lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, wordWidths, wordContinues);
|
||||
}
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
||||
extractLine(i, pageWidth, wordWidths, wordContinues, lineBreakIndices, processLine, renderer, fontId);
|
||||
}
|
||||
|
||||
// Remove consumed words so size() reflects only remaining words
|
||||
@@ -138,8 +137,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth,
|
||||
const int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec) {
|
||||
if (words.empty()) {
|
||||
return {};
|
||||
}
|
||||
@@ -187,9 +185,8 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
// Add space before word j, unless it's the first word on the line or a continuation
|
||||
int gap = 0;
|
||||
if (j > static_cast<size_t>(i) && !continuesVec[j]) {
|
||||
gap = spaceWidth;
|
||||
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]),
|
||||
wordStyles[j - 1]);
|
||||
gap =
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
} else if (j > static_cast<size_t>(i) && continuesVec[j]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
gap = renderer.getKerning(fontId, lastCodepoint(words[j - 1]), firstCodepoint(words[j]), wordStyles[j - 1]);
|
||||
@@ -275,8 +272,7 @@ void ParsedText::applyParagraphIndent() {
|
||||
|
||||
// Builds break indices while opportunistically splitting the word that would overflow the current line.
|
||||
std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId,
|
||||
const int pageWidth, const int spaceWidth,
|
||||
std::vector<uint16_t>& wordWidths,
|
||||
const int pageWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec) {
|
||||
// Calculate first line indent (only for left/justified text).
|
||||
// Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on.
|
||||
@@ -304,9 +300,8 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
const bool isFirstWord = currentIndex == lineStart;
|
||||
int spacing = 0;
|
||||
if (!isFirstWord && !continuesVec[currentIndex]) {
|
||||
spacing = spaceWidth;
|
||||
spacing += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
spacing = renderer.getSpaceAdvance(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
firstCodepoint(words[currentIndex]), wordStyles[currentIndex - 1]);
|
||||
} else if (!isFirstWord && continuesVec[currentIndex]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
spacing = renderer.getKerning(fontId, lastCodepoint(words[currentIndex - 1]),
|
||||
@@ -440,9 +435,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<bool>& continuesVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
const GfxRenderer& renderer, const int fontId) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
@@ -471,11 +465,9 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
// Count gaps: each word after the first creates a gap, unless it's a continuation
|
||||
if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) {
|
||||
actualGapCount++;
|
||||
int naturalGap = spaceWidth;
|
||||
naturalGap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx]),
|
||||
wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
totalNaturalGaps += naturalGap;
|
||||
totalNaturalGaps +=
|
||||
renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx - 1]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx]), wordStyles[lastBreakAt + wordIdx - 1]);
|
||||
} else if (wordIdx > 0 && continuesVec[lastBreakAt + wordIdx]) {
|
||||
// Cross-boundary kerning for continuation words (e.g. nonbreaking spaces, attached punctuation)
|
||||
totalNaturalGaps +=
|
||||
@@ -520,11 +512,11 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]), wordStyles[lastBreakAt + wordIdx]);
|
||||
xpos += advance;
|
||||
} else {
|
||||
int gap = spaceWidth;
|
||||
int gap = 0;
|
||||
if (wordIdx + 1 < lineWordCount) {
|
||||
gap += renderer.getSpaceKernAdjust(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
||||
wordStyles[lastBreakAt + wordIdx]);
|
||||
gap = renderer.getSpaceAdvance(fontId, lastCodepoint(words[lastBreakAt + wordIdx]),
|
||||
firstCodepoint(words[lastBreakAt + wordIdx + 1]),
|
||||
wordStyles[lastBreakAt + wordIdx]);
|
||||
}
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine) {
|
||||
gap += justifyExtra;
|
||||
|
||||
@@ -21,14 +21,13 @@ class ParsedText {
|
||||
bool hyphenationEnabled;
|
||||
|
||||
void applyParagraphIndent();
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth,
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths,
|
||||
std::vector<bool>& continuesVec);
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||
int fontId);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include "../../Epub.h"
|
||||
@@ -758,9 +759,30 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
}
|
||||
}
|
||||
|
||||
// If we're about to run out of space, then cut the word off and start a new one
|
||||
// If we're about to run out of space, then cut the word off and start a new one.
|
||||
// For CJK text (no spaces), this is the primary word-breaking mechanism.
|
||||
// We must avoid splitting multi-byte UTF-8 sequences across word boundaries,
|
||||
// otherwise the trailing bytes become orphaned continuation bytes that the
|
||||
// decoder can't interpret.
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
self->flushPartWordBuffer();
|
||||
int safeLen = utf8SafeTruncateBuffer(self->partWordBuffer, self->partWordBufferIndex);
|
||||
|
||||
if (safeLen < self->partWordBufferIndex && safeLen > 0) {
|
||||
// Incomplete UTF-8 sequence at the end — save it before flushing
|
||||
int overflow = self->partWordBufferIndex - safeLen;
|
||||
char saved[4];
|
||||
for (int j = 0; j < overflow; j++) {
|
||||
saved[j] = self->partWordBuffer[safeLen + j];
|
||||
}
|
||||
self->partWordBufferIndex = safeLen;
|
||||
self->flushPartWordBuffer();
|
||||
for (int j = 0; j < overflow; j++) {
|
||||
self->partWordBuffer[j] = saved[j];
|
||||
}
|
||||
self->partWordBufferIndex = overflow;
|
||||
} else {
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
@@ -772,8 +794,12 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
||||
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
||||
: self->viewportWidth;
|
||||
self->currentTextBlock->layoutAndExtractLines(
|
||||
self->renderer, self->fontId, self->viewportWidth,
|
||||
self->renderer, self->fontId, effectiveWidth,
|
||||
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1046,11 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage.reset(new Page());
|
||||
currentPageNextY = 0;
|
||||
}
|
||||
|
||||
if (currentPageNextY + lineHeight > viewportHeight) {
|
||||
completePageFn(std::move(currentPage));
|
||||
completedPageCount++;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "FontCacheManager.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
FontCacheManager::FontCacheManager(const std::map<int, EpdFontFamily>& fontMap) : fontMap_(fontMap) {}
|
||||
|
||||
void FontCacheManager::setFontDecompressor(FontDecompressor* d) { fontDecompressor_ = d; }
|
||||
|
||||
void FontCacheManager::clearCache() {
|
||||
if (fontDecompressor_) fontDecompressor_->clearCache();
|
||||
}
|
||||
|
||||
void FontCacheManager::prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask) {
|
||||
if (!fontDecompressor_ || fontMap_.count(fontId) == 0) return;
|
||||
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (!(styleMask & (1 << i))) continue;
|
||||
auto style = static_cast<EpdFontFamily::Style>(i);
|
||||
const EpdFontData* data = fontMap_.at(fontId).getData(style);
|
||||
if (!data || !data->groups) continue;
|
||||
int missed = fontDecompressor_->prewarmCache(data, utf8Text);
|
||||
if (missed > 0) {
|
||||
LOG_DBG("FCM", "prewarmCache: %d glyph(s) not cached for style %d", missed, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FontCacheManager::logStats(const char* label) {
|
||||
if (fontDecompressor_) fontDecompressor_->logStats(label);
|
||||
}
|
||||
|
||||
void FontCacheManager::resetStats() {
|
||||
if (fontDecompressor_) fontDecompressor_->resetStats();
|
||||
}
|
||||
|
||||
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
|
||||
|
||||
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
|
||||
scanText_ += text;
|
||||
if (scanFontId_ < 0) scanFontId_ = fontId;
|
||||
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
|
||||
const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
|
||||
uint32_t cpCount = 0;
|
||||
while (*p) {
|
||||
if ((*p & 0xC0) != 0x80) cpCount++;
|
||||
p++;
|
||||
}
|
||||
scanStyleCounts_[baseStyle] += cpCount;
|
||||
}
|
||||
|
||||
// --- PrewarmScope implementation ---
|
||||
|
||||
FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manager_(&manager) {
|
||||
manager_->scanMode_ = ScanMode::Scanning;
|
||||
manager_->clearCache();
|
||||
manager_->resetStats();
|
||||
manager_->scanText_.clear();
|
||||
manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
|
||||
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
|
||||
manager_->scanFontId_ = -1;
|
||||
}
|
||||
|
||||
void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
|
||||
manager_->scanMode_ = ScanMode::None;
|
||||
if (manager_->scanText_.empty()) return;
|
||||
|
||||
// Build style bitmask from all styles that appeared during the scan
|
||||
uint8_t styleMask = 0;
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (manager_->scanStyleCounts_[i] > 0) styleMask |= (1 << i);
|
||||
}
|
||||
if (styleMask == 0) styleMask = 1; // default to regular
|
||||
|
||||
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
|
||||
|
||||
// Free scan string memory
|
||||
manager_->scanText_.clear();
|
||||
manager_->scanText_.shrink_to_fit();
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope::~PrewarmScope() {
|
||||
if (active_) {
|
||||
endScanAndPrewarm(); // no-op if already called (scanText_ is empty)
|
||||
manager_->clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope::PrewarmScope(PrewarmScope&& other) noexcept
|
||||
: manager_(other.manager_), active_(other.active_) {
|
||||
other.active_ = false;
|
||||
}
|
||||
|
||||
FontCacheManager::PrewarmScope FontCacheManager::createPrewarmScope() { return PrewarmScope(*this); }
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class FontDecompressor;
|
||||
|
||||
class FontCacheManager {
|
||||
public:
|
||||
explicit FontCacheManager(const std::map<int, EpdFontFamily>& fontMap);
|
||||
|
||||
void setFontDecompressor(FontDecompressor* d);
|
||||
|
||||
void clearCache();
|
||||
void prewarmCache(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F);
|
||||
void logStats(const char* label = "render");
|
||||
void resetStats();
|
||||
|
||||
// Scan-mode API: called by GfxRenderer::drawText() during scan pass
|
||||
bool isScanning() const;
|
||||
void recordText(const char* text, int fontId, EpdFontFamily::Style style);
|
||||
|
||||
// The FontDecompressor pointer, needed by GfxRenderer::getGlyphBitmap()
|
||||
FontDecompressor* getDecompressor() const { return fontDecompressor_; }
|
||||
|
||||
// RAII scope for two-pass prewarm pattern
|
||||
class PrewarmScope {
|
||||
public:
|
||||
explicit PrewarmScope(FontCacheManager& manager);
|
||||
~PrewarmScope();
|
||||
void endScanAndPrewarm();
|
||||
PrewarmScope(PrewarmScope&& other) noexcept;
|
||||
PrewarmScope& operator=(PrewarmScope&&) = delete;
|
||||
PrewarmScope(const PrewarmScope&) = delete;
|
||||
PrewarmScope& operator=(const PrewarmScope&) = delete;
|
||||
|
||||
private:
|
||||
FontCacheManager* manager_;
|
||||
bool active_ = true;
|
||||
};
|
||||
PrewarmScope createPrewarmScope();
|
||||
|
||||
private:
|
||||
const std::map<int, EpdFontFamily>& fontMap_;
|
||||
FontDecompressor* fontDecompressor_ = nullptr;
|
||||
|
||||
enum class ScanMode : uint8_t { None, Scanning };
|
||||
ScanMode scanMode_ = ScanMode::None;
|
||||
std::string scanText_;
|
||||
uint32_t scanStyleCounts_[4] = {};
|
||||
int scanFontId_ = -1;
|
||||
};
|
||||
@@ -1,18 +1,23 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <cstring>
|
||||
#include "FontCacheManager.h"
|
||||
|
||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||
if (fontData->groups != nullptr) {
|
||||
if (!fontDecompressor) {
|
||||
auto* fd = fontCacheManager_ ? fontCacheManager_->getDecompressor() : nullptr;
|
||||
if (!fd) {
|
||||
LOG_ERR("GFX", "Compressed font but no FontDecompressor set");
|
||||
return nullptr;
|
||||
}
|
||||
uint16_t glyphIndex = static_cast<uint16_t>(glyph - fontData->glyph);
|
||||
return fontDecompressor->getBitmap(fontData, glyph, glyphIndex);
|
||||
uint32_t glyphIndex = static_cast<uint32_t>(glyph - fontData->glyph);
|
||||
// For page-buffer hits the pointer is stable for the page lifetime.
|
||||
// For hot-group hits it is valid only until the next getBitmap() call — callers
|
||||
// must consume it (draw the glyph) before requesting another bitmap.
|
||||
return fd->getBitmap(fontData, glyph, glyphIndex);
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
@@ -749,6 +754,11 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) {
|
||||
fontCacheManager_->recordText(text, fontId, style);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
@@ -884,6 +894,7 @@ void GfxRenderer::drawText2BitLegacy(const int fontId, const int x, const int y,
|
||||
#endif // ENABLE_RENDERCHAR_BENCHMARK
|
||||
|
||||
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
if (x1 == x2) {
|
||||
if (y2 < y1) {
|
||||
std::swap(y1, y2);
|
||||
@@ -1355,6 +1366,7 @@ void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, con
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
||||
const float cropX, const float cropY) const {
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
|
||||
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
|
||||
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
|
||||
@@ -1729,13 +1741,18 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
||||
return spaceGlyph ? fp4::toPixel(spaceGlyph->advanceX) : 0; // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceKernAdjust(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const auto& font = fontIt->second;
|
||||
const int kernFP = font.getKerning(leftCp, ' ', style) + font.getKerning(' ', rightCp, style); // 4.4 fixed-point
|
||||
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
||||
const int32_t spaceAdvanceFP = spaceGlyph ? static_cast<int32_t>(spaceGlyph->advanceX) : 0;
|
||||
// Combine space advance + flanking kern into one fixed-point sum before snapping.
|
||||
// Snapping the combined value avoids the +/-1 px error from snapping each component separately.
|
||||
const int32_t kernFP = static_cast<int32_t>(font.getKerning(leftCp, ' ', style)) +
|
||||
static_cast<int32_t>(font.getKerning(' ', rightCp, style));
|
||||
return fp4::toPixel(spaceAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalDisplay.h>
|
||||
|
||||
class FontCacheManager;
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -39,7 +41,14 @@ class GfxRenderer {
|
||||
uint8_t* frameBuffer = nullptr;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
FontDecompressor* fontDecompressor = nullptr;
|
||||
|
||||
// Mutable because drawText() is const but needs to delegate scan-mode
|
||||
// recording to the (non-const) FontCacheManager. Same pragmatic compromise
|
||||
// as before, concentrated in a single pointer instead of four fields.
|
||||
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
||||
|
||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
||||
EpdFontFamily::Style style) const;
|
||||
void freeBwBufferChunks();
|
||||
template <Color color>
|
||||
void drawPixelDither(int x, int y) const;
|
||||
@@ -67,10 +76,9 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
void setFontDecompressor(FontDecompressor* d) { fontDecompressor = d; }
|
||||
void clearFontCache() {
|
||||
if (fontDecompressor) fontDecompressor->clearCache();
|
||||
}
|
||||
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
|
||||
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
|
||||
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
@@ -118,9 +126,10 @@ class GfxRenderer {
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
/// Returns the kerning adjustment for a space between two codepoints:
|
||||
/// kern(leftCp, ' ') + kern(' ', rightCp). Returns 0 if kerning is unavailable.
|
||||
int getSpaceKernAdjust(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
|
||||
/// Returns the total inter-word advance: fp4::toPixel(spaceAdvance + kern(leftCp,' ') + kern(' ',rightCp)).
|
||||
/// Using a single snap avoids the +/-1 px rounding error that arises when space advance and kern are
|
||||
/// snapped separately and then added as integers.
|
||||
int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
|
||||
/// Returns the kerning adjustment between two adjacent codepoints.
|
||||
int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
|
||||
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
_language_name: "Қазақша"
|
||||
_language_code: "KK"
|
||||
_order: "18"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "ЖҮКТЕЛУДЕ"
|
||||
STR_SLEEPING: "ҰЙҚЫ РЕЖИМІ"
|
||||
STR_ENTERING_SLEEP: "Ұйқы режиміне өту"
|
||||
STR_BROWSE_FILES: "Файлдар"
|
||||
STR_FILE_TRANSFER: "Файл жіберу"
|
||||
STR_SETTINGS_TITLE: "Баптаулар"
|
||||
STR_CALIBRE_LIBRARY: "Calibre кітапханасы"
|
||||
STR_CONTINUE_READING: "Оқуды жалғастыру"
|
||||
STR_NO_OPEN_BOOK: "Ашық кітап жоқ"
|
||||
STR_START_READING: "Төменде оқуды бастаңыз"
|
||||
STR_BOOKS: "Кітаптар"
|
||||
STR_NO_BOOKS_FOUND: "Кітаптар табылмады"
|
||||
STR_SELECT_CHAPTER: "Тарауды таңдаңыз"
|
||||
STR_NO_CHAPTERS: "Тараулар жоқ"
|
||||
STR_END_OF_BOOK: "Кітап аяқталды"
|
||||
STR_EMPTY_CHAPTER: "Бос тарау"
|
||||
STR_INDEXING: "Индекстелуде"
|
||||
STR_MEMORY_ERROR: "Жад қатесі"
|
||||
STR_PAGE_LOAD_ERROR: "Бетті жүктеу қатесі"
|
||||
STR_EMPTY_FILE: "Бос файл"
|
||||
STR_OUT_OF_BOUNDS: "Шектен тыс"
|
||||
STR_LOADING: "Жүктелуде..."
|
||||
STR_LOADING_POPUP: "Жүктелуде"
|
||||
STR_LOAD_XTC_FAILED: "XTC жүктеу сәтсіз"
|
||||
STR_LOAD_TXT_FAILED: "TXT жүктеу сәтсіз"
|
||||
STR_LOAD_EPUB_FAILED: "EPUB жүктеу сәтсіз"
|
||||
STR_SD_CARD_ERROR: "SD карта қатесі"
|
||||
STR_WIFI_NETWORKS: "WiFi желілері"
|
||||
STR_NO_NETWORKS: "Желілер табылмады"
|
||||
STR_NETWORKS_FOUND: "%zu желі табылды"
|
||||
STR_SCANNING: "Іздеуде..."
|
||||
STR_CONNECTING: "Қосылуда..."
|
||||
STR_CONNECTED: "Қосылды!"
|
||||
STR_CONNECTION_FAILED: "Қосылу сәтсіз"
|
||||
STR_CONNECTION_TIMEOUT: "Қосылу уақыты өтті"
|
||||
STR_FORGET_NETWORK: "Желіні ұмыту?"
|
||||
STR_SAVE_PASSWORD: "Келесі жолға құпия сөзді сақтау керек пе?"
|
||||
STR_REMOVE_PASSWORD: "Сақталған құпия сөзді жою керек пе?"
|
||||
STR_PRESS_OK_SCAN: "Қайта іздеу үшін OK басыңыз"
|
||||
STR_PRESS_ANY_CONTINUE: "Жалғастыру үшін кез келген түймені басыңыз"
|
||||
STR_SELECT_HINT: "СОЛ/ОҢ: Таңдау | OK: Растау"
|
||||
STR_HOW_CONNECT: "Қалай қосылғыңыз келеді?"
|
||||
STR_JOIN_NETWORK: "Желіге қосылу"
|
||||
STR_CREATE_HOTSPOT: "Хотспот жасау"
|
||||
STR_JOIN_DESC: "Бар WiFi желісіне қосылу"
|
||||
STR_HOTSPOT_DESC: "Басқалар қоса алатын WiFi желісін жасау"
|
||||
STR_STARTING_HOTSPOT: "Хотспот іске қосылуда..."
|
||||
STR_HOTSPOT_MODE: "Хотспот режимі"
|
||||
STR_CONNECT_WIFI_HINT: "Құрылғыңызды осы WiFi желісіне қосыңыз"
|
||||
STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашыңыз"
|
||||
STR_OR_HTTP_PREFIX: "немесе http://"
|
||||
STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre сымсыз"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_CONNECT_WIRELESS: "Сымсыз құрылғы ретінде қосылу"
|
||||
STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған"
|
||||
STR_MAC_ADDRESS: "MAC мекенжайы:"
|
||||
STR_CHECKING_WIFI: "WiFi тексерілуде..."
|
||||
STR_ENTER_WIFI_PASSWORD: "WiFi құпия сөзін енгізіңіз"
|
||||
STR_ENTER_TEXT: "Мәтін енгізіңіз"
|
||||
STR_TO_PREFIX: ""
|
||||
STR_CALIBRE_DISCOVERING: "Calibre іздеуде..."
|
||||
STR_CALIBRE_CONNECTING_TO: "Қосылуда: "
|
||||
STR_CALIBRE_CONNECTED_TO: "Қосылды: "
|
||||
STR_CALIBRE_WAITING_COMMANDS: "Пәрмендер күтілуде..."
|
||||
STR_CONNECTION_FAILED_RETRYING: "(Қосылу сәтсіз, қайталануда)"
|
||||
STR_CALIBRE_DISCONNECTED: "Calibre ажыратылды"
|
||||
STR_CALIBRE_WAITING_TRANSFER: "Тасымалдау күтілуде..."
|
||||
STR_CALIBRE_TRANSFER_HINT: "Тасымалдау сәтсіз болса, Calibre\\nSmartDevice плагин параметрлерінде\\n'Бос орынды елемеу' қосыңыз."
|
||||
STR_CALIBRE_RECEIVING: "Қабылдануда: "
|
||||
STR_CALIBRE_RECEIVED: "Қабылданды: "
|
||||
STR_CALIBRE_WAITING_MORE: "Жалғасы күтілуде..."
|
||||
STR_CALIBRE_FAILED_CREATE_FILE: "Файл жасау сәтсіз"
|
||||
STR_CALIBRE_PASSWORD_REQUIRED: "Құпия сөз қажет"
|
||||
STR_CALIBRE_TRANSFER_INTERRUPTED: "Тасымалдау үзілді"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader плагинін орнатыңыз"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Бір WiFi желісінде болыңыз"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) Calibre-де: \"Құрылғыға жіберу\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Жіберу кезінде осы экранды ашық ұстаңыз\""
|
||||
STR_CAT_DISPLAY: "Дисплей"
|
||||
STR_CAT_READER: "Оқырман"
|
||||
STR_CAT_CONTROLS: "Басқару"
|
||||
STR_CAT_SYSTEM: "Жүйе"
|
||||
STR_SLEEP_SCREEN: "Ұйқы экраны"
|
||||
STR_SLEEP_COVER_MODE: "Ұйқы экраны мұқаба режимі"
|
||||
STR_STATUS_BAR: "Күй жолағы"
|
||||
STR_HIDE_BATTERY: "Батарея % жасыру"
|
||||
STR_EXTRA_SPACING: "Қосымша абзац аралығы"
|
||||
STR_TEXT_AA: "Мәтін сырғытпасы"
|
||||
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
|
||||
STR_ORIENTATION: "Оқу бағдары"
|
||||
STR_FRONT_BTN_LAYOUT: "Алдыңғы түймелер орналасуы"
|
||||
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
||||
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
||||
STR_FONT_FAMILY: "Оқырман қаріп тобы"
|
||||
STR_EXT_READER_FONT: "Сыртқы оқырман қарібі"
|
||||
STR_EXT_CHINESE_FONT: "Оқырман қарібі"
|
||||
STR_EXT_UI_FONT: "Интерфейс қарібі"
|
||||
STR_FONT_SIZE: "Интерфейс қаріп өлшемі"
|
||||
STR_LINE_SPACING: "Оқырман жол аралығы"
|
||||
STR_ASCII_LETTER_SPACING: "ASCII әріп аралығы"
|
||||
STR_ASCII_DIGIT_SPACING: "ASCII сан аралығы"
|
||||
STR_CJK_SPACING: "CJK аралығы"
|
||||
STR_COLOR_MODE: "Түс режимі"
|
||||
STR_SCREEN_MARGIN: "Оқырман экран жиегі"
|
||||
STR_PARA_ALIGNMENT: "Оқырман абзац туралануы"
|
||||
STR_HYPHENATION: "Буын бөлу"
|
||||
STR_TIME_TO_SLEEP: "Ұйқы уақыты"
|
||||
STR_REFRESH_FREQ: "Жаңарту жиілігі"
|
||||
STR_CALIBRE_SETTINGS: "Calibre параметрлері"
|
||||
STR_KOREADER_SYNC: "KOReader синхронизациясы"
|
||||
STR_CHECK_UPDATES: "Жаңартуларды тексеру"
|
||||
STR_LANGUAGE: "Тіл"
|
||||
STR_SELECT_WALLPAPER: "Тұсқағаз таңдау"
|
||||
STR_CLEAR_READING_CACHE: "Оқу кэшін тазалау"
|
||||
STR_CALIBRE: "Calibre"
|
||||
STR_USERNAME: "Пайдаланушы аты"
|
||||
STR_PASSWORD: "Құпия сөз"
|
||||
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
||||
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
||||
STR_AUTHENTICATE: "Аутентификация"
|
||||
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
||||
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
||||
STR_FILENAME: "Файл аты"
|
||||
STR_BINARY: "Бинарлық"
|
||||
STR_SET_CREDENTIALS_FIRST: "Алдымен тіркелгі деректерін орнатыңыз"
|
||||
STR_WIFI_CONN_FAILED: "WiFi қосылуы сәтсіз"
|
||||
STR_AUTHENTICATING: "Аутентификацияланып жатыр..."
|
||||
STR_AUTH_SUCCESS: "Аутентификация сәтті!"
|
||||
STR_KOREADER_AUTH: "KOReader аутентификациясы"
|
||||
STR_SYNC_READY: "KOReader синхронизациясы пайдалануға дайын"
|
||||
STR_AUTH_FAILED: "Аутентификация сәтсіз"
|
||||
STR_DONE: "Дайын"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Бұл барлық кэштелген кітап деректерін жояды."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Барлық оқу үлгерімі жоғалады!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Кітаптарды қайта индекстеу қажет болады"
|
||||
STR_CLEAR_CACHE_WARNING_4: "келесі ашылғанда."
|
||||
STR_CLEARING_CACHE: "Кэш тазалануда..."
|
||||
STR_CACHE_CLEARED: "Кэш тазаланды"
|
||||
STR_ITEMS_REMOVED: "элемент жойылды"
|
||||
STR_FAILED_LOWER: "сәтсіз"
|
||||
STR_CLEAR_CACHE_FAILED: "Кэшті тазалау сәтсіз"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Толық ақпарат үшін сериялық шығысты тексеріңіз"
|
||||
STR_DARK: "Қараңғы"
|
||||
STR_LIGHT: "Ашық"
|
||||
STR_CUSTOM: "Өзгертілген"
|
||||
STR_COVER: "Мұқаба"
|
||||
STR_NONE_OPT: "Ешқайсысы"
|
||||
STR_FIT: "Сыйдыру"
|
||||
STR_CROP: "Кесу"
|
||||
STR_NO_PROGRESS: "Үлгерім жоқ"
|
||||
STR_FULL_OPT: "Толық"
|
||||
STR_NEVER: "Ешқашан"
|
||||
STR_IN_READER: "Оқырманда"
|
||||
STR_ALWAYS: "Әрқашан"
|
||||
STR_IGNORE: "Елемеу"
|
||||
STR_SLEEP: "Ұйқы"
|
||||
STR_PAGE_TURN: "Бет аудару"
|
||||
STR_PORTRAIT: "Тік бағдар"
|
||||
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
|
||||
STR_INVERTED: "Төңкерілген"
|
||||
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
|
||||
STR_FRONT_LAYOUT_BCLR: "Арт, Раст, Сол, Оң"
|
||||
STR_FRONT_LAYOUT_LRBC: "Сол, Оң, Арт, Раст"
|
||||
STR_FRONT_LAYOUT_LBCR: "Сол, Арт, Раст, Оң"
|
||||
STR_PREV_NEXT: "Алдыңғы/Келесі"
|
||||
STR_NEXT_PREV: "Келесі/Алдыңғы"
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_OPEN_DYSLEXIC: "Open Dyslexic"
|
||||
STR_SMALL: "Кішкентай"
|
||||
STR_MEDIUM: "Орташа"
|
||||
STR_LARGE: "Үлкен"
|
||||
STR_X_LARGE: "Өте үлкен"
|
||||
STR_TIGHT: "Тар"
|
||||
STR_NORMAL: "Қалыпты"
|
||||
STR_WIDE: "Кең"
|
||||
STR_JUSTIFY: "Тегістеу"
|
||||
STR_ALIGN_LEFT: "Солға"
|
||||
STR_CENTER: "Ортаға"
|
||||
STR_ALIGN_RIGHT: "Оңға"
|
||||
STR_MIN_1: "1 мин"
|
||||
STR_MIN_5: "5 мин"
|
||||
STR_MIN_10: "10 мин"
|
||||
STR_MIN_15: "15 мин"
|
||||
STR_MIN_30: "30 мин"
|
||||
STR_PAGES_1: "1 бет"
|
||||
STR_PAGES_5: "5 бет"
|
||||
STR_PAGES_10: "10 бет"
|
||||
STR_PAGES_15: "15 бет"
|
||||
STR_PAGES_30: "30 бет"
|
||||
STR_UPDATE: "Жаңарту"
|
||||
STR_CHECKING_UPDATE: "Жаңарту тексерілуде..."
|
||||
STR_NEW_UPDATE: "Жаңа жаңарту бар!"
|
||||
STR_CURRENT_VERSION: "Ағымдағы нұсқа: "
|
||||
STR_NEW_VERSION: "Жаңа нұсқа: "
|
||||
STR_UPDATING: "Жаңартылуда..."
|
||||
STR_NO_UPDATE: "Жаңарту жоқ"
|
||||
STR_UPDATE_FAILED: "Жаңарту сәтсіз"
|
||||
STR_UPDATE_COMPLETE: "Жаңарту аяқталды"
|
||||
STR_POWER_ON_HINT: "Қайта қосу үшін қуат түймесін басып ұстаңыз"
|
||||
STR_EXTERNAL_FONT: "Сыртқы қаріп"
|
||||
STR_BUILTIN_DISABLED: "Кірістірілген (өшірілген)"
|
||||
STR_NO_ENTRIES: "Жазбалар табылмады"
|
||||
STR_DOWNLOADING: "Жүктеп алынуда..."
|
||||
STR_DOWNLOAD_FAILED: "Жүктеп алу сәтсіз"
|
||||
STR_ERROR_MSG: "Қате:"
|
||||
STR_UNNAMED: "Атаусыз"
|
||||
STR_NO_SERVER_URL: "Сервер URL конфигурацияланмаған"
|
||||
STR_FETCH_FEED_FAILED: "Ақынды алу сәтсіз"
|
||||
STR_PARSE_FEED_FAILED: "Ақынды талдау сәтсіз"
|
||||
STR_NETWORK_PREFIX: "Желі: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP мекенжайы: "
|
||||
STR_SCAN_QR_WIFI_HINT: "немесе WiFi-ға қосылу үшін телефонмен QR кодын сканерлеңіз."
|
||||
STR_ERROR_GENERAL_FAILURE: "Қате: Жалпы сәтсіздік"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Қате: Желі табылмады"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Қате: Қосылу уақыты өтті"
|
||||
STR_SD_CARD: "SD карта"
|
||||
STR_BACK: "« Артқа"
|
||||
STR_EXIT: "« Шығу"
|
||||
STR_HOME: "« Басты"
|
||||
STR_SAVE: "« Сақтау"
|
||||
STR_SELECT: "Таңдау"
|
||||
STR_TOGGLE: "Ауыстыру"
|
||||
STR_CONFIRM: "Растау"
|
||||
STR_CANCEL: "Болдырмау"
|
||||
STR_CONNECT: "Қосылу"
|
||||
STR_OPEN: "Ашу"
|
||||
STR_DOWNLOAD: "Жүктеп алу"
|
||||
STR_RETRY: "Қайталау"
|
||||
STR_YES: "Иә"
|
||||
STR_NO: "Жоқ"
|
||||
STR_STATE_ON: "ҚОСУЛЫ"
|
||||
STR_STATE_OFF: "ӨШІРУЛІ"
|
||||
STR_SET: "Орнату"
|
||||
STR_NOT_SET: "Орнатылмаған"
|
||||
STR_DIR_LEFT: "Сол"
|
||||
STR_DIR_RIGHT: "Оң"
|
||||
STR_DIR_UP: "Жоғары"
|
||||
STR_DIR_DOWN: "Төмен"
|
||||
STR_CAPS_ON: "БАС"
|
||||
STR_CAPS_OFF: "кіші"
|
||||
STR_OK_BUTTON: "ОК"
|
||||
STR_ON_MARKER: "[ҚОСУЛЫ]"
|
||||
STR_SLEEP_COVER_FILTER: "Ұйқы экраны мұқаба сүзгісі"
|
||||
STR_FILTER_CONTRAST: "Контраст"
|
||||
STR_STATUS_BAR_FULL_PERCENT: "Толық пайызбен"
|
||||
STR_STATUS_BAR_FULL_BOOK: "Толық кітап жолағымен"
|
||||
STR_STATUS_BAR_BOOK_ONLY: "Тек кітап жолағы"
|
||||
STR_STATUS_BAR_FULL_CHAPTER: "Толық тарау жолағымен"
|
||||
STR_UI_THEME: "Интерфейс тақырыбы"
|
||||
STR_THEME_CLASSIC: "Классикалық"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra кеңейтілген"
|
||||
STR_SUNLIGHT_FADING_FIX: "Күн сәулесінен солу түзету"
|
||||
STR_REMAP_FRONT_BUTTONS: "Алдыңғы түймелерді қайта баптау"
|
||||
STR_OPDS_BROWSER: "OPDS шолғышы"
|
||||
STR_COVER_CUSTOM: "Мұқаба + Өзгертілген"
|
||||
STR_RECENTS: "Соңғылар"
|
||||
STR_MENU_RECENT_BOOKS: "Жуырда оқылған кітаптар"
|
||||
STR_NO_RECENT_BOOKS: "Жуырда оқылған кітаптар жоқ"
|
||||
STR_CALIBRE_DESC: "Calibre сымсыз тасымалдауын пайдалану"
|
||||
STR_FORGET_AND_REMOVE: "Желіні ұмыту және сақталған құпия сөзді жою керек пе?"
|
||||
STR_FORGET_BUTTON: "Ұмыту"
|
||||
STR_CALIBRE_STARTING: "Calibre іске қосылуда..."
|
||||
STR_CALIBRE_SETUP: "Баптау"
|
||||
STR_CALIBRE_STATUS: "Күй"
|
||||
STR_CLEAR_BUTTON: "Тазалау"
|
||||
STR_DEFAULT_VALUE: "Әдепкі"
|
||||
STR_REMAP_PROMPT: "Әр рөл үшін алдыңғы түймені басыңыз"
|
||||
STR_UNASSIGNED: "Тағайындалмаған"
|
||||
STR_ALREADY_ASSIGNED: "Қазірдің өзінде тағайындалған"
|
||||
STR_REMAP_RESET_HINT: "Бүйірлік түйме жоғары: Әдепкі орналасуды қалпына келтіру"
|
||||
STR_REMAP_CANCEL_HINT: "Бүйірлік түйме төмен: Қайта баптауды болдырмау"
|
||||
STR_HW_BACK_LABEL: "Артқа (1-ші түйме)"
|
||||
STR_HW_CONFIRM_LABEL: "Растау (2-ші түйме)"
|
||||
STR_HW_LEFT_LABEL: "Сол (3-ші түйме)"
|
||||
STR_HW_RIGHT_LABEL: "Оң (4-ші түйме)"
|
||||
STR_GO_TO_PERCENT: "%-ке өту"
|
||||
STR_GO_HOME_BUTTON: "Басты бетке өту"
|
||||
STR_SYNC_PROGRESS: "Үлгерімді синхрондау"
|
||||
STR_DELETE_CACHE: "Кітап кэшін жою"
|
||||
STR_CHAPTER_PREFIX: "Тарау: "
|
||||
STR_PAGES_SEPARATOR: " бет | "
|
||||
STR_BOOK_PREFIX: "Кітап: "
|
||||
STR_KBD_SHIFT: "shift"
|
||||
STR_KBD_SHIFT_CAPS: "SHIFT"
|
||||
STR_KBD_LOCK: "LOCK"
|
||||
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
|
||||
STR_PERCENT_STEP_HINT: "Сол/Оң: 1% Жоғары/Төмен: 10%"
|
||||
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
|
||||
STR_CALC_HASH: "Құжат хэші есептелуде..."
|
||||
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
|
||||
STR_FETCH_PROGRESS: "Қашықтағы үлгерім алынуда..."
|
||||
STR_UPLOAD_PROGRESS: "Үлгерім жүктеп салынуда..."
|
||||
STR_NO_CREDENTIALS_MSG: "Тіркелгі деректері конфигурацияланмаған"
|
||||
STR_KOREADER_SETUP_HINT: "Параметрлерде KOReader тіркелгісін баптаңыз"
|
||||
STR_PROGRESS_FOUND: "Үлгерім табылды!"
|
||||
STR_REMOTE_LABEL: "Қашықтағы:"
|
||||
STR_LOCAL_LABEL: "Жергілікті:"
|
||||
STR_PAGE_OVERALL_FORMAT: "%d-бет, жалпы %.2f%%"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "%d/%d-бет, жалпы %.2f%%"
|
||||
STR_DEVICE_FROM_FORMAT: " Бастап: %s"
|
||||
STR_APPLY_REMOTE: "Қашықтағы үлгерімді қолдану"
|
||||
STR_UPLOAD_LOCAL: "Жергілікті үлгерімді жүктеп салу"
|
||||
STR_NO_REMOTE_MSG: "Қашықтағы үлгерім табылмады"
|
||||
STR_UPLOAD_PROMPT: "Ағымдағы орынды жүктеп салу керек пе?"
|
||||
STR_UPLOAD_SUCCESS: "Үлгерім жүктеп салынды!"
|
||||
STR_SYNC_FAILED_MSG: "Синхрондау сәтсіз"
|
||||
STR_SECTION_PREFIX: "Бөлім "
|
||||
STR_UPLOAD: "Жүктеп салу"
|
||||
STR_BOOK_S_STYLE: "Кітап стилі"
|
||||
STR_EMBEDDED_STYLE: "Кірістірілген стиль"
|
||||
STR_OPDS_SERVER_URL: "OPDS сервері URL"
|
||||
STR_NO_FILES_FOUND: "Файлдар табылмады"
|
||||
STR_IMAGES: "Суреттер"
|
||||
STR_IMAGES_DISPLAY: "Көрсету"
|
||||
STR_IMAGES_PLACEHOLDER: "Орын белгіші"
|
||||
STR_IMAGES_SUPPRESS: "Жасыру"
|
||||
STR_SELECTED: "Таңдалды"
|
||||
STR_SHOW: "Көрсету"
|
||||
STR_HIDE: "Жасыру"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Күй жолағын баптау"
|
||||
STR_CHAPTER_PAGE_COUNT: "Тараудың бет саны"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Кітап үлгерімі пайызы"
|
||||
STR_PROGRESS_BAR: "Үлгерім жолағы"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Үлгерім жолағының қалыңдығы"
|
||||
STR_PROGRESS_BAR_THIN: "Жіңішке"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Орташа"
|
||||
STR_PROGRESS_BAR_THICK: "Қалың"
|
||||
STR_BOOK: "Кітап"
|
||||
STR_CHAPTER: "Тарау"
|
||||
STR_EXAMPLE_CHAPTER: "21-тарау"
|
||||
STR_EXAMPLE_BOOK: "Кітап атауы"
|
||||
STR_PREVIEW: "Алдын ала қарау"
|
||||
STR_TITLE: "Атау"
|
||||
STR_BATTERY: "Батарея"
|
||||
STR_DELETE: "Жою"
|
||||
STR_DISPLAY_QR: "Бетті QR ретінде көрсету"
|
||||
STR_FOOTNOTES: "Түсіндірме жазбалар"
|
||||
STR_NO_FOOTNOTES: "Бұл бетте түсіндірме жазбалар жоқ"
|
||||
STR_LINK: "[сілтеме]"
|
||||
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
||||
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
||||
@@ -43,7 +43,7 @@ STR_SAVE_PASSWORD: "¿Guardar contraseña?"
|
||||
STR_REMOVE_PASSWORD: "¿Olvidar contraseña?"
|
||||
STR_PRESS_OK_SCAN: "Pulse OK para buscar de nuevo"
|
||||
STR_PRESS_ANY_CONTINUE: "Pulse cualquier botón para continuar"
|
||||
STR_SELECT_HINT: "Izq./Der.: Seleccionar | OK: Confirmar"
|
||||
STR_SELECT_HINT: "Izq./Dcha.: Seleccionar | OK: Confirmar"
|
||||
STR_HOW_CONNECT: "¿Cómo desea conectarse?"
|
||||
STR_JOIN_NETWORK: "Unirse a una red"
|
||||
STR_CREATE_HOTSPOT: "Crear punto de acceso"
|
||||
@@ -92,6 +92,10 @@ STR_STATUS_BAR: "Barra de estado"
|
||||
STR_HIDE_BATTERY: "Ocultar % de batería"
|
||||
STR_EXTRA_SPACING: "Espaciado entre párrafos"
|
||||
STR_TEXT_AA: "Suavizado de texto"
|
||||
STR_IMAGES: "Imágenes"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||
STR_SHORT_PWR_BTN: "Toque corto botón encendido"
|
||||
STR_ORIENTATION: "Orientación"
|
||||
STR_FRONT_BTN_LAYOUT: "Diseño de los botones frontales"
|
||||
@@ -165,9 +169,9 @@ STR_PORTRAIT: "Vertical"
|
||||
STR_LANDSCAPE_CW: "Horizontal (horario)"
|
||||
STR_INVERTED: "Invertido"
|
||||
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
|
||||
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izq., Der."
|
||||
STR_FRONT_LAYOUT_LRBC: "Izq., Der., Atrás, Confirmar"
|
||||
STR_FRONT_LAYOUT_LBCR: "Izq., Atrás, Confirmar, Der."
|
||||
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izq., Dcha."
|
||||
STR_FRONT_LAYOUT_LRBC: "Izq., Dcha., Atrás, Confirmar"
|
||||
STR_FRONT_LAYOUT_LBCR: "Izq., Atrás, Confirmar, Dcha."
|
||||
STR_PREV_NEXT: "Ant./Sig."
|
||||
STR_NEXT_PREV: "Sig./Ant."
|
||||
STR_BOOKERLY: "Bookerly"
|
||||
@@ -225,7 +229,7 @@ STR_BACK: "« Atrás"
|
||||
STR_EXIT: "« Salir"
|
||||
STR_HOME: "« Inicio"
|
||||
STR_SAVE: "« Guardar"
|
||||
STR_SELECT: "Selec."
|
||||
STR_SELECT: "Selecc."
|
||||
STR_SELECTED: "Seleccionado"
|
||||
STR_TOGGLE: "Cambiar"
|
||||
STR_CONFIRM: "Confirmar"
|
||||
@@ -242,7 +246,7 @@ STR_STATE_ON: "Activado"
|
||||
STR_STATE_OFF: "Desactivado"
|
||||
STR_NOT_SET: "No configurado"
|
||||
STR_DIR_LEFT: "Izq."
|
||||
STR_DIR_RIGHT: "Der."
|
||||
STR_DIR_RIGHT: "Dcha."
|
||||
STR_DIR_UP: "Subir"
|
||||
STR_DIR_DOWN: "Bajar"
|
||||
STR_CAPS_ON: "MAYÚSCULAS"
|
||||
@@ -251,7 +255,7 @@ STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtro de pantalla de suspensión"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado"
|
||||
STR_CHAPTER_PAGE_COUNT: "Contador pág. cap."
|
||||
STR_CHAPTER_PAGE_COUNT: "Contador de pág. por cap."
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Porcentaje progreso libro"
|
||||
STR_PROGRESS_BAR: "Barra de progreso"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Grosor de barra de progreso"
|
||||
@@ -292,7 +296,7 @@ STR_REMAP_CANCEL_HINT: "Botón lateral abajo: anular reconfiguración"
|
||||
STR_HW_BACK_LABEL: "Atrás (primer botón)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirmar (segundo botón)"
|
||||
STR_HW_LEFT_LABEL: "Izq. (tercer botón)"
|
||||
STR_HW_RIGHT_LABEL: "Der. (cuarto botón)"
|
||||
STR_HW_RIGHT_LABEL: "Dcha. (cuarto botón)"
|
||||
STR_GO_TO_PERCENT: "Ir a %"
|
||||
STR_GO_HOME_BUTTON: "Volver al menú Inicio"
|
||||
STR_SYNC_PROGRESS: "Sincronizar progreso de lectura"
|
||||
@@ -300,13 +304,13 @@ STR_DELETE_CACHE: "Borrar caché del libro"
|
||||
STR_DELETE: "Borrar"
|
||||
STR_DISPLAY_QR: "Mostrar página como QR"
|
||||
STR_CHAPTER_PREFIX: "Cap.: "
|
||||
STR_PAGES_SEPARATOR: " Págs. | "
|
||||
STR_PAGES_SEPARATOR: " pág. | "
|
||||
STR_BOOK_PREFIX: "Libro: "
|
||||
STR_KBD_SHIFT: "minús."
|
||||
STR_KBD_SHIFT_CAPS: "MAYÚS."
|
||||
STR_KBD_LOCK: "BLOQUEAR"
|
||||
STR_KBD_SHIFT: "minús"
|
||||
STR_KBD_SHIFT_CAPS: "MAYÚS"
|
||||
STR_KBD_LOCK: "BLOQ"
|
||||
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
|
||||
STR_PERCENT_STEP_HINT: "Izq./Der.: 1% | Subir/Bajar: 10%"
|
||||
STR_PERCENT_STEP_HINT: "Izq./Dcha.: 1% | Subir/Bajar: 10%"
|
||||
STR_SYNCING_TIME: "Tiempo de sincronización..."
|
||||
STR_CALC_HASH: "Calculando hash del documento..."
|
||||
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
|
||||
@@ -335,5 +339,5 @@ STR_FOOTNOTES: "Pie de página"
|
||||
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
|
||||
STR_LINK: "[enlace]"
|
||||
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
|
||||
STR_AUTO_TURN_ENABLED: "Paso pág. automático: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Páginas por minuto"
|
||||
STR_AUTO_TURN_ENABLED: "Páginas por minuto: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Leer páginas por minuto"
|
||||
|
||||
@@ -14,10 +14,7 @@ RTC_NOINIT_ATTR size_t logHead = 0;
|
||||
// value is only set by clearLastLogs(), so its absence means the buffer was
|
||||
// never properly initialized.
|
||||
RTC_NOINIT_ATTR uint32_t rtcLogMagic;
|
||||
static constexpr uint32_t fnv1a32(const char* s, uint32_t h = 2166136261u) {
|
||||
return *s ? fnv1a32(s + 1, (h ^ static_cast<uint32_t>(*s)) * 16777619u) : h;
|
||||
}
|
||||
static constexpr uint32_t LOG_RTC_MAGIC = fnv1a32("crosspoint-reader");
|
||||
static constexpr uint32_t LOG_RTC_MAGIC = 0xDEADBEEF;
|
||||
|
||||
void addToLogRingBuffer(const char* message) {
|
||||
// Add the message to the ring buffer, overwriting old messages if necessary.
|
||||
|
||||
+48
-2
@@ -13,23 +13,69 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int bytes = utf8CodepointLen(**string);
|
||||
const unsigned char lead = **string;
|
||||
const int bytes = utf8CodepointLen(lead);
|
||||
const uint8_t* chr = *string;
|
||||
*string += bytes;
|
||||
|
||||
// Invalid lead byte (stray continuation byte 0x80-0xBF, or 0xFE/0xFF)
|
||||
if (bytes == 1 && lead >= 0x80) {
|
||||
(*string)++;
|
||||
return REPLACEMENT_GLYPH;
|
||||
}
|
||||
|
||||
if (bytes == 1) {
|
||||
(*string)++;
|
||||
return chr[0];
|
||||
}
|
||||
|
||||
// Validate continuation bytes before consuming them
|
||||
for (int i = 1; i < bytes; i++) {
|
||||
if ((chr[i] & 0xC0) != 0x80) {
|
||||
// Missing or invalid continuation byte — skip all bytes consumed so far
|
||||
*string += i;
|
||||
return REPLACEMENT_GLYPH;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t cp = chr[0] & ((1 << (7 - bytes)) - 1); // mask header bits
|
||||
|
||||
for (int i = 1; i < bytes; i++) {
|
||||
cp = (cp << 6) | (chr[i] & 0x3F);
|
||||
}
|
||||
|
||||
// Reject overlong encodings, surrogates, and out-of-range values
|
||||
const bool overlong = (bytes == 2 && cp < 0x80) || (bytes == 3 && cp < 0x800) || (bytes == 4 && cp < 0x10000);
|
||||
const bool surrogate = (cp >= 0xD800 && cp <= 0xDFFF);
|
||||
if (overlong || surrogate || cp > 0x10FFFF) {
|
||||
(*string)++;
|
||||
return REPLACEMENT_GLYPH;
|
||||
}
|
||||
|
||||
*string += bytes;
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
int utf8SafeTruncateBuffer(const char* buf, int len) {
|
||||
if (len <= 0) return 0;
|
||||
|
||||
// Walk back past continuation bytes (10xxxxxx) to find the lead byte
|
||||
int leadPos = len - 1;
|
||||
while (leadPos > 0 && (static_cast<uint8_t>(buf[leadPos]) & 0xC0) == 0x80) {
|
||||
leadPos--;
|
||||
}
|
||||
|
||||
// Determine expected length of the sequence starting at leadPos
|
||||
int expectedLen = utf8CodepointLen(static_cast<unsigned char>(buf[leadPos]));
|
||||
int actualLen = len - leadPos;
|
||||
|
||||
if (actualLen < expectedLen && leadPos > 0) {
|
||||
// Incomplete UTF-8 sequence at the end — exclude it
|
||||
return leadPos;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
size_t utf8RemoveLastChar(std::string& str) {
|
||||
if (str.empty()) return 0;
|
||||
size_t pos = str.size() - 1;
|
||||
|
||||
@@ -10,6 +10,11 @@ size_t utf8RemoveLastChar(std::string& str);
|
||||
// Truncate string by removing N UTF-8 codepoints from the end.
|
||||
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||
|
||||
// Truncate a raw char buffer to the last complete UTF-8 codepoint boundary.
|
||||
// Returns the new length (<= len). If the buffer ends mid-sequence, the
|
||||
// incomplete trailing bytes are excluded.
|
||||
int utf8SafeTruncateBuffer(const char* buf, int len);
|
||||
|
||||
// Returns true for Unicode combining diacritical marks that should not advance the cursor.
|
||||
inline bool utf8IsCombiningMark(const uint32_t cp) {
|
||||
return (cp >= 0x0300 && cp <= 0x036F) // Combining Diacritical Marks
|
||||
|
||||
@@ -332,6 +332,7 @@ int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
int matched = 0;
|
||||
const int targetCount = static_cast<int>(targets.size());
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
@@ -372,6 +373,10 @@ int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
if (matched >= targetCount) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@ LANG_ABBREVIATIONS = {
|
||||
"فارسی": "FA", "persian": "FA",
|
||||
"čeština": "CS",
|
||||
"türkçe": "TR", "turkish": "TR",
|
||||
"Қазақша": "KK", "kazakh": "KK",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ bool CrossPointState::loadFromBinaryFile() {
|
||||
if (version >= 2) {
|
||||
serialization::readPod(inputFile, lastSleepImage);
|
||||
} else {
|
||||
lastSleepImage = 0;
|
||||
lastSleepImage = UINT8_MAX;
|
||||
}
|
||||
|
||||
if (version >= 3) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
@@ -8,7 +9,7 @@ class CrossPointState {
|
||||
|
||||
public:
|
||||
std::string openEpubPath;
|
||||
uint8_t lastSleepImage;
|
||||
uint8_t lastSleepImage = UINT8_MAX; // UINT8_MAX = unset sentinel
|
||||
uint8_t readerActivityLoadCount = 0;
|
||||
bool lastSleepFromReader = false;
|
||||
~CrossPointState() = default;
|
||||
|
||||
@@ -87,7 +87,7 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) {
|
||||
}
|
||||
|
||||
s.openEpubPath = doc["openEpubPath"] | std::string("");
|
||||
s.lastSleepImage = doc["lastSleepImage"] | (uint8_t)0;
|
||||
s.lastSleepImage = doc["lastSleepImage"] | (uint8_t)UINT8_MAX;
|
||||
s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0;
|
||||
s.lastSleepFromReader = doc["lastSleepFromReader"] | false;
|
||||
return true;
|
||||
|
||||
@@ -231,7 +231,7 @@ void SleepActivity::renderCustomSleepScreen() const {
|
||||
// Generate a random number between 1 and numFiles
|
||||
auto randomFileIndex = random(numFiles);
|
||||
// If we picked the same image as last time, reroll
|
||||
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
|
||||
while (numFiles > 1 && APP_STATE.lastSleepImage != UINT8_MAX && randomFileIndex == APP_STATE.lastSleepImage) {
|
||||
randomFileIndex = random(numFiles);
|
||||
}
|
||||
APP_STATE.lastSleepImage = randomFileIndex;
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/blocks/TextBlock.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -19,6 +21,7 @@
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "QrDisplayActivity.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@@ -27,7 +30,6 @@
|
||||
namespace {
|
||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||
constexpr unsigned long skipChapterMs = 700;
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
||||
|
||||
@@ -41,27 +43,6 @@ int clampPercent(int percent) {
|
||||
return percent;
|
||||
}
|
||||
|
||||
// Apply the logical reader orientation to the renderer.
|
||||
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
||||
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||
switch (orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EpubReaderActivity::onEnter() {
|
||||
@@ -73,7 +54,7 @@ void EpubReaderActivity::onEnter() {
|
||||
|
||||
// Configure screen orientation based on settings
|
||||
// NOTE: This affects layout math and must be applied before any render calls.
|
||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
epub->setupCacheDir();
|
||||
|
||||
@@ -183,13 +164,14 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
if (footnoteDepth > 0) {
|
||||
restoreSavedPosition();
|
||||
return;
|
||||
@@ -198,20 +180,7 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool nextTriggered = usePressForPageTurn
|
||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -459,7 +428,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
||||
SETTINGS.saveToFile();
|
||||
|
||||
// Update renderer orientation to match the new logical coordinate system.
|
||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
// Reset section to force re-layout in the new orientation.
|
||||
section.reset();
|
||||
@@ -669,7 +638,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
const auto start = millis();
|
||||
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
||||
renderer.clearFontCache();
|
||||
}
|
||||
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
||||
|
||||
@@ -699,11 +667,30 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||
const int orientedMarginRight, const int orientedMarginBottom,
|
||||
const int orientedMarginLeft) {
|
||||
const auto t0 = millis();
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
fcm->resetStats();
|
||||
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
const uint32_t heapBefore = esp_get_free_heap_size();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass
|
||||
scope.endScanAndPrewarm();
|
||||
const uint32_t heapAfter = esp_get_free_heap_size();
|
||||
fcm->logStats("prewarm");
|
||||
const auto tPrewarm = millis();
|
||||
|
||||
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter,
|
||||
(int32_t)heapAfter - (int32_t)heapBefore);
|
||||
|
||||
// Force special handling for pages with images when anti-aliasing is on
|
||||
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderStatusBar();
|
||||
fcm->logStats("bw_render");
|
||||
const auto tBwRender = millis();
|
||||
|
||||
if (imagePageWithAA) {
|
||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
||||
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
|
||||
@@ -723,16 +710,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
||||
} else if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
}
|
||||
const auto tDisplay = millis();
|
||||
|
||||
// Save bw buffer to reset buffer state after grayscale data sync
|
||||
renderer.storeBwBuffer();
|
||||
const auto tBwStore = millis();
|
||||
|
||||
// grayscale rendering
|
||||
// TODO: Only do this if font supports it
|
||||
@@ -741,20 +726,42 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
const auto tGrayLsb = millis();
|
||||
|
||||
// Render and copy to MSB buffer
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
const auto tGrayMsb = millis();
|
||||
|
||||
// display grayscale part
|
||||
renderer.displayGrayBuffer();
|
||||
const auto tGrayDisplay = millis();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
}
|
||||
fcm->logStats("gray");
|
||||
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums "
|
||||
"gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
|
||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||
} else {
|
||||
// restore the bw data
|
||||
renderer.restoreBwBuffer();
|
||||
const auto tBwRestore = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore,
|
||||
tEnd - t0);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderStatusBar() const {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include <CrossPointSettings.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
namespace ReaderUtils {
|
||||
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
|
||||
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||
switch (orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct PageTurnResult {
|
||||
bool prev;
|
||||
bool next;
|
||||
};
|
||||
|
||||
inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
||||
const bool usePress = !SETTINGS.longPressChapterSkip;
|
||||
const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
input.wasPressed(MappedInputManager::Button::Left))
|
||||
: (input.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
input.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||
input.wasPressed(MappedInputManager::Button::Right))
|
||||
: (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||
input.wasReleased(MappedInputManager::Button::Right));
|
||||
return {prev, next};
|
||||
}
|
||||
|
||||
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
}
|
||||
}
|
||||
|
||||
// Grayscale anti-aliasing pass. Renders content twice (LSB + MSB) to build
|
||||
// the grayscale buffer. Only the content callback is re-rendered — status bars
|
||||
// and other overlays should be drawn before calling this.
|
||||
// Kept as a template to avoid std::function overhead; instantiated once per reader type.
|
||||
template <typename RenderFn>
|
||||
void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) {
|
||||
if (!renderer.storeBwBuffer()) {
|
||||
LOG_ERR("READER", "Failed to store BW buffer for anti-aliasing");
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
renderFn();
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
renderFn();
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
|
||||
renderer.restoreBwBuffer();
|
||||
}
|
||||
|
||||
} // namespace ReaderUtils
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "TxtReaderActivity.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
@@ -9,14 +10,13 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||
|
||||
// Cache file magic and version
|
||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||
@@ -88,23 +88,7 @@ void TxtReaderActivity::onEnter() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure screen orientation based on settings
|
||||
switch (SETTINGS.orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
txt->setupCacheDir();
|
||||
|
||||
@@ -134,31 +118,19 @@ void TxtReaderActivity::onExit() {
|
||||
|
||||
void TxtReaderActivity::loop() {
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||
return;
|
||||
}
|
||||
|
||||
// Short press BACK goes directly to home
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
|
||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
||||
const bool nextTriggered = usePressForPageTurn
|
||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
@@ -316,7 +288,6 @@ void TxtReaderActivity::render(RenderLock&&) {
|
||||
|
||||
renderer.clearScreen();
|
||||
renderPage();
|
||||
renderer.clearFontCache();
|
||||
|
||||
// Save progress
|
||||
saveProgress();
|
||||
@@ -361,39 +332,22 @@ void TxtReaderActivity::renderPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// First pass: BW rendering
|
||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||
auto* fcm = renderer.getFontCacheManager();
|
||||
auto scope = fcm->createPrewarmScope();
|
||||
renderLines(); // scan pass — text accumulated, no drawing
|
||||
scope.endScanAndPrewarm();
|
||||
|
||||
// BW rendering
|
||||
renderLines();
|
||||
renderStatusBar();
|
||||
|
||||
if (pagesUntilFullRefresh <= 1) {
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||
} else {
|
||||
renderer.displayBuffer();
|
||||
pagesUntilFullRefresh--;
|
||||
}
|
||||
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||
|
||||
// Grayscale rendering pass (for anti-aliased fonts)
|
||||
if (SETTINGS.textAntiAliasing) {
|
||||
// Save BW buffer for restoration after grayscale pass
|
||||
renderer.storeBwBuffer();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
renderLines();
|
||||
renderer.copyGrayscaleLsbBuffers();
|
||||
|
||||
renderer.clearScreen(0x00);
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||
renderLines();
|
||||
renderer.copyGrayscaleMsbBuffers();
|
||||
|
||||
renderer.displayGrayBuffer();
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
|
||||
// Restore BW buffer
|
||||
renderer.restoreBwBuffer();
|
||||
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
|
||||
}
|
||||
// scope destructor clears font cache via FontCacheManager
|
||||
}
|
||||
|
||||
void TxtReaderActivity::renderStatusBar() const {
|
||||
|
||||
@@ -89,8 +89,13 @@ void SettingsActivity::loop() {
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
SETTINGS.saveToFile();
|
||||
onGoHome();
|
||||
if (selectedSettingIndex > 0) {
|
||||
selectedSettingIndex = 0;
|
||||
requestUpdate();
|
||||
} else {
|
||||
SETTINGS.saveToFile();
|
||||
onGoHome();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
#include <Arduino.h>
|
||||
#include <Epub.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
@@ -32,6 +33,7 @@ MappedInputManager mappedInputManager(gpio);
|
||||
GfxRenderer renderer(display);
|
||||
ActivityManager activityManager(renderer, mappedInputManager);
|
||||
FontDecompressor fontDecompressor;
|
||||
FontCacheManager fontCacheManager(renderer.getFontMap());
|
||||
|
||||
// Fonts
|
||||
EpdFont bookerly14RegularFont(&bookerly_14_regular);
|
||||
@@ -203,7 +205,8 @@ void setupDisplayAndFonts() {
|
||||
if (!fontDecompressor.init()) {
|
||||
LOG_ERR("MAIN", "Font decompressor init failed");
|
||||
}
|
||||
renderer.setFontDecompressor(&fontDecompressor);
|
||||
fontCacheManager.setFontDecompressor(&fontDecompressor);
|
||||
renderer.setFontCacheManager(&fontCacheManager);
|
||||
renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily);
|
||||
#ifndef OMIT_FONTS
|
||||
renderer.insertFont(BOOKERLY_12_FONT_ID, bookerly12FontFamily);
|
||||
|
||||
+14
-2
@@ -1,5 +1,6 @@
|
||||
#include "QrUtils.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -12,7 +13,18 @@ void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const
|
||||
// Version 4 holds ~114 bytes, Version 10 ~395, Version 20 ~1066, up to 40
|
||||
// qrcode.h max version is 40.
|
||||
// Formula: approx version = size / 26 + 1 (very rough estimate, better to find best fit)
|
||||
const size_t len = textPayload.length();
|
||||
size_t len = textPayload.length();
|
||||
|
||||
// Truncate to max QR capacity at a UTF-8 safe boundary to avoid splitting multi-byte sequences
|
||||
static constexpr size_t MAX_QR_CAPACITY = 2953; // Version 40, ECC_LOW, byte mode
|
||||
std::string truncated;
|
||||
const char* payload = textPayload.c_str();
|
||||
if (len > MAX_QR_CAPACITY) {
|
||||
len = utf8SafeTruncateBuffer(textPayload.c_str(), static_cast<int>(MAX_QR_CAPACITY));
|
||||
truncated = textPayload.substr(0, len);
|
||||
payload = truncated.c_str();
|
||||
}
|
||||
|
||||
int version = 4;
|
||||
if (len > 114) version = 10;
|
||||
if (len > 395) version = 20;
|
||||
@@ -25,7 +37,7 @@ void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const
|
||||
|
||||
QRCode qrcode;
|
||||
// Initialize the QR code. We use ECC_LOW for max capacity.
|
||||
int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, textPayload.c_str());
|
||||
int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, payload);
|
||||
|
||||
if (res == 0) {
|
||||
// Determine the optimal pixel size.
|
||||
|
||||
Reference in New Issue
Block a user