## Purpose This PR includes some preparatory changes that are needed for an upcoming performant CJK font feature. The changes have no impact on render time and heap allocation for latin text. **Despite this, I think these changes stand on their own as a better font compression/decompression implementation.** ## Summary - Font decompressor rewrite: Replaced the 4-slot LRU group cache with a two-tier system — a page buffer (glyphs prewarmed before rendering begins) and a hot-group fallback (last decompressed group retained for non-prewarmed glyphs). - Byte-aligned compressed bitmap format: Glyph bitmaps within compressed groups are now stored row-padded rather than tightly packed before DEFLATE compression, improving compression ratios by making identical pixel rows produce identical byte patterns. Glyphs are compacted back to packed format on demand at render time. Reduces flash size by 155 KB. - Page prewarm system: Added `Page::collectText` and `Page::getDominantStyle` to extract per-style glyph requirements before rendering, and `GfxRenderer::prewarmFontCache` to pre-decompress only the groups needed for the dominant style — eliminating mid-render decompression for the common case. - UTF-8 robustness fixes: `utf8NextCodepoint` now validates continuation bytes and returns a replacement glyph on malformed input; `ChapterHtmlSlimParser` correctly preserves incomplete multi-byte sequences across word-buffer flush boundaries rather than splitting them. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ Architecture and design was done by me, refined a bit by Claude. Code mostly by Claude, but not entirely.
113 lines
3.9 KiB
C++
113 lines
3.9 KiB
C++
#pragma once
|
|
#include <HalStorage.h>
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "FootnoteEntry.h"
|
|
#include "blocks/ImageBlock.h"
|
|
#include "blocks/TextBlock.h"
|
|
|
|
enum PageElementTag : uint8_t {
|
|
TAG_PageLine = 1,
|
|
TAG_PageImage = 2, // New tag
|
|
};
|
|
|
|
// represents something that has been added to a page
|
|
class PageElement {
|
|
public:
|
|
int16_t xPos;
|
|
int16_t yPos;
|
|
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
|
virtual ~PageElement() = default;
|
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
|
virtual bool serialize(FsFile& file) = 0;
|
|
virtual PageElementTag getTag() const = 0; // Add type identification
|
|
};
|
|
|
|
// a line from a block element
|
|
class PageLine final : public PageElement {
|
|
std::shared_ptr<TextBlock> block;
|
|
|
|
public:
|
|
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
|
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
|
bool serialize(FsFile& file) override;
|
|
PageElementTag getTag() const override { return TAG_PageLine; }
|
|
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
|
};
|
|
|
|
// New PageImage class
|
|
class PageImage final : public PageElement {
|
|
std::shared_ptr<ImageBlock> imageBlock;
|
|
|
|
public:
|
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
|
bool serialize(FsFile& file) override;
|
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
|
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
|
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
|
};
|
|
|
|
class Page {
|
|
public:
|
|
// the list of block index and line numbers on this page
|
|
std::vector<std::shared_ptr<PageElement>> elements;
|
|
std::vector<FootnoteEntry> footnotes;
|
|
static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16;
|
|
|
|
void addFootnote(const char* number, const char* href) {
|
|
if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes
|
|
FootnoteEntry entry;
|
|
strncpy(entry.number, number, sizeof(entry.number) - 1);
|
|
entry.number[sizeof(entry.number) - 1] = '\0';
|
|
strncpy(entry.href, href, sizeof(entry.href) - 1);
|
|
entry.href[sizeof(entry.href) - 1] = '\0';
|
|
footnotes.push_back(entry);
|
|
}
|
|
|
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
|
bool serialize(FsFile& file) const;
|
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
|
|
|
// Check if page contains any images (used to force full refresh)
|
|
bool hasImages() const {
|
|
return std::any_of(elements.begin(), elements.end(),
|
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
|
}
|
|
|
|
// Get bounding box of all images on the page (union of image rects)
|
|
// Returns false if no images. Coordinates are relative to page origin.
|
|
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
|
bool found = false;
|
|
int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN;
|
|
for (const auto& el : elements) {
|
|
if (el->getTag() == TAG_PageImage) {
|
|
const auto& img = static_cast<const PageImage&>(*el);
|
|
int16_t x = img.xPos;
|
|
int16_t y = img.yPos;
|
|
int16_t right = x + img.getImageBlock().getWidth();
|
|
int16_t bottom = y + img.getImageBlock().getHeight();
|
|
minX = std::min(minX, x);
|
|
minY = std::min(minY, y);
|
|
maxX = std::max(maxX, right);
|
|
maxY = std::max(maxY, bottom);
|
|
found = true;
|
|
}
|
|
}
|
|
if (found) {
|
|
outX = minX;
|
|
outY = minY;
|
|
outW = maxX - minX;
|
|
outH = maxY - minY;
|
|
}
|
|
return found;
|
|
}
|
|
};
|