Fix glyph cache poisoning across renders; add CLAUDE.md and handoff.md
fontkit caches glyphs per font for the whole process and pdfkit embeds composite components with no code points, so setting ':' before '.' in Poppins corrupted every later render in the same process (text layer showed '1,200;00', line breaks shifted). primeFontGlyphs() now creates the component glyphs with their cmap code point at font registration, awaited by renderCore and browser init. Regression test and Quirk 13 added; the warm-up workarounds in the template tests are removed.
This commit is contained in:
@@ -31,6 +31,78 @@ export function registerBundledFonts(base: string): void {
|
||||
Font.registerHyphenationCallback((word) => [word]);
|
||||
}
|
||||
|
||||
/** The slice of a fontkit TTF font the priming touches (fontkit's types do not declare it). */
|
||||
interface FkFont {
|
||||
numGlyphs: number;
|
||||
characterSet: number[];
|
||||
glyphForCodePoint(cp: number): unknown;
|
||||
_getBaseGlyph(id: number): { _decode?(): { components?: Array<{ glyphID: number }> } | null };
|
||||
_glyphs: Record<number, unknown>;
|
||||
_cmapProcessor: { lookup(cp: number): number };
|
||||
}
|
||||
|
||||
const isPrivateUse = (cp: number) => cp >= 0xe000 && cp <= 0xf8ff;
|
||||
|
||||
/**
|
||||
* Creates, through the cmap and so with their code point, every glyph that a composite glyph uses as a
|
||||
* component. Not the whole cmap: a glyph that layout substitutes in (Inter's contextual hyphen between digits)
|
||||
* is mapped from a private-use code point, and creating it from there would give it a code point the text
|
||||
* never had. Components that only a private-use code point reaches are left alone.
|
||||
*/
|
||||
function primeComponents(font: FkFont): void {
|
||||
const components = new Set<number>();
|
||||
// _getBaseGlyph caches what it creates, with no code points; scan into a scratch cache so that does not happen.
|
||||
const cache = font._glyphs;
|
||||
font._glyphs = {};
|
||||
try {
|
||||
for (let id = 0; id < font.numGlyphs; id++) {
|
||||
const decoded = font._getBaseGlyph(id)._decode?.();
|
||||
for (const c of decoded?.components ?? []) components.add(c.glyphID);
|
||||
}
|
||||
} finally {
|
||||
font._glyphs = cache;
|
||||
}
|
||||
const lowest = new Map<number, number>();
|
||||
for (const cp of [...font.characterSet].sort((a, b) => a - b)) {
|
||||
if (isPrivateUse(cp)) continue;
|
||||
const id = font._cmapProcessor.lookup(cp);
|
||||
if (components.has(id) && !lowest.has(id)) lowest.set(id, cp);
|
||||
}
|
||||
for (const cp of lowest.values()) font.glyphForCodePoint(cp);
|
||||
}
|
||||
|
||||
let primed: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Loads every registered face and gives the component glyphs of its composites their code points. fontkit
|
||||
* caches glyph objects per font for the whole process, and pdfkit creates a composite's components at embed
|
||||
* time with no code points: Poppins' colon contains the period, so a document that sets ':' before any '.'
|
||||
* would leave the period glyph with codePoints [] for every later document (amounts extracted as "1,200;00",
|
||||
* textkit's line breaks slipping). Creating those glyphs here first makes a render independent of the renders
|
||||
* before it. Memoised; await it before the first render.
|
||||
*/
|
||||
export function primeFontGlyphs(): Promise<void> {
|
||||
if (!registered) return Promise.resolve();
|
||||
if (!primed) {
|
||||
primed = run().catch((err) => {
|
||||
primed = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return primed;
|
||||
|
||||
async function run(): Promise<void> {
|
||||
for (const fam of FONT_MANIFEST.families) {
|
||||
for (const s of familySources(fam)) {
|
||||
const descriptor = { fontFamily: fam.family, fontWeight: s.fontWeight, fontStyle: s.fontStyle };
|
||||
await Font.load(descriptor);
|
||||
const data = (Font as unknown as { getFont(d: object): { data: FkFont | null } }).getFont(descriptor).data;
|
||||
if (data) primeComponents(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_FONT_BYTES = 10 * 1024;
|
||||
|
||||
function hasTrueTypeMagic(b: Uint8Array): boolean {
|
||||
|
||||
Reference in New Issue
Block a user