Document migration plan from CrossPoint's lib/Epub to FreeInkBook, a memory-efficient clean-room EPUB engine. Covers advantages (O(paragraph+page) memory via SAX parsing, arena allocation, host-testable, full UAX#14 typography, runtime TTF/OTF support), 9-step migration strategy, ESP32-C3 constraints, and integration approach preserving existing UI layer while replacing parsing/layout stack.
207 lines
7.4 KiB
C++
207 lines
7.4 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <cstdint>
|
|
|
|
#include "CpFontAdapter.h"
|
|
#include "lib/EpdFont/EpdFont.h"
|
|
#include "lib/EpdFont/EpdFontData.h"
|
|
#include "lib/EpdFont/EpdFontFamily.h"
|
|
#include "lib/Utf8/Utf8.h"
|
|
|
|
// ============================================================================
|
|
// Synthetic fonts (same convention as DifferentialRoundingTest): metrics
|
|
// chosen so per-glyph rounding and differential rounding disagree unless the
|
|
// adapter's kerning-delta trick is applied.
|
|
//
|
|
// Glyphs: 'T' (0x54), 'a' (0x61), 'o' (0x6F), 'x' (0x78), U+FFFD.
|
|
// Kern pairs (4.4 FP): T->a -5, T->o -7, o->a -2, o->o -3.
|
|
// Ligature: 'T'+'x' -> U+E000 (private use; also a glyph in the font).
|
|
// ============================================================================
|
|
|
|
namespace {
|
|
|
|
// clang-format off
|
|
const EpdGlyph kGlyphs16[] = {
|
|
/* 0 'T' */ { 8, 12, 137, 0, 12, 0, 0 },
|
|
/* 1 'a' */ { 7, 8, 130, 0, 8, 0, 0 },
|
|
/* 2 'o' */ { 8, 8, 145, 0, 8, 0, 0 },
|
|
/* 3 'x' */ { 7, 8, 136, 0, 8, 0, 0 },
|
|
/* 4 U+E000 */ { 12, 12, 200, 0, 12, 0, 0 },
|
|
/* 5 U+FFFD */ { 9, 12, 160, 0, 12, 0, 0 },
|
|
};
|
|
|
|
const EpdUnicodeInterval kIntervals16[] = {
|
|
{ 0x54, 0x54, 0 },
|
|
{ 0x61, 0x61, 1 },
|
|
{ 0x6F, 0x6F, 2 },
|
|
{ 0x78, 0x78, 3 },
|
|
{ 0xE000, 0xE000, 4 },
|
|
{ 0xFFFD, 0xFFFD, 5 },
|
|
};
|
|
|
|
const EpdKernClassEntry kKernLeft[] = { { 0x54, 1 }, { 0x6F, 2 } };
|
|
const EpdKernClassEntry kKernRight[] = { { 0x61, 1 }, { 0x6F, 2 } };
|
|
const int8_t kKernMatrix[] = { -5, -7, -2, -3 };
|
|
|
|
const EpdLigaturePair kLigatures[] = {
|
|
{ (0x54u << 16) | 0x78u, 0xE000 }, // 'T' + 'x'
|
|
};
|
|
// clang-format on
|
|
|
|
EpdFontData makeFontData(const EpdGlyph* glyphs, const EpdUnicodeInterval* intervals,
|
|
uint32_t intervalCount, uint8_t advanceY, int ascender, int descender) {
|
|
EpdFontData d{};
|
|
d.bitmap = nullptr;
|
|
d.glyph = glyphs;
|
|
d.intervals = intervals;
|
|
d.intervalCount = intervalCount;
|
|
d.advanceY = advanceY;
|
|
d.ascender = ascender;
|
|
d.descender = descender;
|
|
d.kernLeftClasses = kKernLeft;
|
|
d.kernRightClasses = kKernRight;
|
|
d.kernMatrix = kKernMatrix;
|
|
d.kernLeftEntryCount = 2;
|
|
d.kernRightEntryCount = 2;
|
|
d.kernLeftClassCount = 2;
|
|
d.kernRightClassCount = 2;
|
|
d.ligaturePairs = kLigatures;
|
|
d.ligaturePairCount = 1;
|
|
return d;
|
|
}
|
|
|
|
// A second, wider font standing in for the 12 px ladder rung so quantization
|
|
// is observable ('T' advance differs).
|
|
const EpdGlyph kGlyphs12[] = {
|
|
/* 0 'T' */ {6, 9, 96, 0, 9, 0, 0}, // 6.0 px exactly
|
|
};
|
|
const EpdUnicodeInterval kIntervals12[] = {{0x54, 0x54, 0}};
|
|
|
|
// The reference: GfxRenderer's differential-rounding cursor walk
|
|
// (drawText/getTextWidth in GfxRenderer.cpp) — each step snaps
|
|
// (prev advance FP + kern FP) as one unit.
|
|
int rendererWidth(const EpdFontFamily& family, const uint32_t* cps, int n,
|
|
EpdFontFamily::Style style) {
|
|
int width = 0;
|
|
int32_t prevAdvanceFP = 0;
|
|
uint32_t prevCp = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
const uint32_t cp = cps[i];
|
|
if (prevCp != 0) {
|
|
width += fp4::toPixel(prevAdvanceFP + family.getKerning(prevCp, cp, style));
|
|
}
|
|
const EpdGlyph* glyph = family.getGlyph(cp, style);
|
|
prevAdvanceFP = glyph ? glyph->advanceX : 0;
|
|
prevCp = cp;
|
|
}
|
|
width += fp4::toPixel(prevAdvanceFP);
|
|
return width;
|
|
}
|
|
|
|
// The engine's accounting: advance(cp) plus kerning(prev, cp) per glyph
|
|
// (ChapterLayout::advanceFor).
|
|
int layoutWidth(CpFontAdapter& font, const uint32_t* cps, int n, uint16_t sizePx) {
|
|
int width = 0;
|
|
uint32_t prevCp = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
width += font.advance(cps[i], sizePx, 0);
|
|
if (prevCp != 0) width += font.kerning(prevCp, cps[i], sizePx, 0);
|
|
prevCp = cps[i];
|
|
}
|
|
return width;
|
|
}
|
|
|
|
class CpFontAdapterTest : public ::testing::Test {
|
|
protected:
|
|
CpFontAdapterTest()
|
|
: data16_(makeFontData(kGlyphs16, kIntervals16, 6, 20, 14, -5)),
|
|
data12_(makeFontData(kGlyphs12, kIntervals12, 1, 15, 11, -4)),
|
|
font16_(&data16_),
|
|
font12_(&data12_),
|
|
family16_(&font16_),
|
|
family12_(&font12_) {
|
|
adapter_.addSize(12, &family12_);
|
|
adapter_.addSize(16, &family16_);
|
|
}
|
|
|
|
EpdFontData data16_, data12_;
|
|
EpdFont font16_, font12_;
|
|
EpdFontFamily family16_, family12_;
|
|
CpFontAdapter adapter_;
|
|
};
|
|
|
|
TEST_F(CpFontAdapterTest, AdvanceSnapsFixedPointPerGlyph) {
|
|
// 137 FP -> 9 px, 130 -> 8, 145 -> 9, 136 -> 9 (round-half-up at 8/16).
|
|
EXPECT_EQ(adapter_.advance('T', 16, 0), 9);
|
|
EXPECT_EQ(adapter_.advance('a', 16, 0), 8);
|
|
EXPECT_EQ(adapter_.advance('o', 16, 0), 9);
|
|
EXPECT_EQ(adapter_.advance('x', 16, 0), 9);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, KerningDeltaMatchesRendererDifferentialRounding) {
|
|
// Every string the renderer can draw must measure to the renderer's own
|
|
// differentially-rounded width — the telescoping identity the adapter's
|
|
// kerning() implements. "oo" (advance frac 1, kern -3) is the case where
|
|
// naive per-glyph rounding diverges.
|
|
const uint32_t oo[] = {'o', 'o'};
|
|
const uint32_t tao[] = {'T', 'a', 'o'};
|
|
const uint32_t tooao[] = {'T', 'o', 'o', 'a', 'o'};
|
|
EXPECT_EQ(layoutWidth(adapter_, oo, 2, 16), rendererWidth(family16_, oo, 2, EpdFontFamily::REGULAR));
|
|
EXPECT_EQ(layoutWidth(adapter_, tao, 3, 16), rendererWidth(family16_, tao, 3, EpdFontFamily::REGULAR));
|
|
EXPECT_EQ(layoutWidth(adapter_, tooao, 5, 16),
|
|
rendererWidth(family16_, tooao, 5, EpdFontFamily::REGULAR));
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, KerningPairValues) {
|
|
// kerning(T,o) = toPixel(137 + (-7)) - toPixel(137) = 8 - 9 = -1.
|
|
EXPECT_EQ(adapter_.kerning('T', 'o', 16, 0), -1);
|
|
// kerning(o,o) = toPixel(145 - 3) - toPixel(145) = 9 - 9 = 0.
|
|
EXPECT_EQ(adapter_.kerning('o', 'o', 16, 0), 0);
|
|
// No kern class pair -> 0.
|
|
EXPECT_EQ(adapter_.kerning('a', 'x', 16, 0), 0);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, QuantizesToNearestRungInMetricsToo) {
|
|
EXPECT_EQ(adapter_.quantize(12), 12);
|
|
EXPECT_EQ(adapter_.quantize(13), 12);
|
|
EXPECT_EQ(adapter_.quantize(14), 12); // tie keeps the smaller rung
|
|
EXPECT_EQ(adapter_.quantize(15), 16);
|
|
EXPECT_EQ(adapter_.quantize(24), 16); // above the ladder clamps to the top
|
|
// 'T' at 12 px comes from the 12 px font (96 FP -> 6 px), not a scale.
|
|
EXPECT_EQ(adapter_.advance('T', 12, 0), 6);
|
|
EXPECT_EQ(adapter_.advance('T', 13, 0), 6);
|
|
EXPECT_EQ(adapter_.advance('T', 24, 0), 9);
|
|
EXPECT_EQ(adapter_.lineHeight(13), 15);
|
|
EXPECT_EQ(adapter_.lineHeight(24), 20);
|
|
EXPECT_EQ(adapter_.ascent(13), 11);
|
|
EXPECT_EQ(adapter_.ascent(24), 14);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, HasGlyphIsNotFooledByReplacementFallback) {
|
|
EXPECT_TRUE(adapter_.hasGlyph('T'));
|
|
EXPECT_TRUE(adapter_.hasGlyph(0xFFFD));
|
|
// 'q' is missing; getGlyph would return U+FFFD, hasGlyph must say no.
|
|
EXPECT_FALSE(adapter_.hasGlyph('q'));
|
|
EXPECT_NE(family16_.getGlyph('q', EpdFontFamily::REGULAR), nullptr);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, LigatureLookup) {
|
|
EXPECT_EQ(adapter_.ligature('T', 'x', 0), 0xE000u);
|
|
EXPECT_EQ(adapter_.ligature('T', 'a', 0), 0u);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, CombiningMarksDoNotAdvance) {
|
|
// U+0301 combining acute: the renderer centers it over the base glyph and
|
|
// moves the cursor zero pixels; layout must agree.
|
|
EXPECT_EQ(adapter_.advance(0x0301, 16, 0), 0);
|
|
EXPECT_EQ(adapter_.kerning('a', 0x0301, 16, 0), 0);
|
|
}
|
|
|
|
TEST_F(CpFontAdapterTest, MissingGlyphAdvanceMatchesRendererReplacementDraw) {
|
|
// The renderer draws U+FFFD for a missing codepoint; layout must reserve
|
|
// the replacement glyph's width (160 FP -> 10 px), not zero.
|
|
EXPECT_EQ(adapter_.advance('q', 16, 0), 10);
|
|
}
|
|
|
|
} // namespace
|