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:
2026-10-04 13:11:40 +05:30
parent a2152ae3c6
commit f13665922c
9 changed files with 308 additions and 25 deletions
+69
View File
@@ -0,0 +1,69 @@
/**
* Rendering must not depend on earlier renders in the same process (src/pdf/quirks.test.tsx, quirk 13).
* This file runs in a fresh module context, so the first document below really is the first one that
* touches Poppins Regular.
*/
import { createElement as h } from "react";
import { Font, Text } from "@react-pdf/renderer";
import { describe, expect, it } from "vitest";
import { doc } from "../testing/docs";
import { parsePdfText } from "../testing/pdfText";
import { renderCore } from "../render/core";
import { setupPdfTest } from "../testing/setup";
import { FONT_MANIFEST } from "./manifest";
import { familySources, stack } from "./register";
setupPdfTest();
type Glyph = { id: number; codePoints: number[]; components?: Array<{ glyphID: number }> };
type FkFont = { characterSet: number[]; glyphForCodePoint(cp: number): Glyph; getGlyph(id: number): Glyph; numGlyphs: number };
const fontOf = (family: string, fontWeight: number, fontStyle: string) =>
(Font as unknown as { getFont: (d: object) => { data: FkFont } }).getFont({ fontFamily: family, fontWeight, fontStyle }).data;
const poppins = { fontFamily: stack("Poppins"), fontSize: 10, lineHeight: "12pt", fontWeight: 400 };
describe("glyph priming", () => {
it("a colon set in Poppins Regular before any period does not stop a later amount being extracted as text", async () => {
await renderCore(doc(h(Text, { style: poppins }, "Bank : State")));
const r = await renderCore(doc(h(Text, { style: poppins }, "₹1,200.00")));
const pages = await parsePdfText(r.bytes);
const text = pages.pages.flatMap((p) => p.items.map((i) => i.str)).join("");
expect(text).toContain("₹1,200.00");
});
it("the period glyph keeps its code point", () => {
expect(fontOf("Poppins", 400, "normal").glyphForCodePoint(0x2e).codePoints).toEqual([0x2e]);
});
it("every glyph that a composite uses as a component is created with a code point, in every bundled face", async () => {
// Reads fontkit's glyph cache and cmap directly: asking for a glyph through the public API would create it with code points and hide the bug.
type Peek = {
numGlyphs: number;
_glyphs: Record<number, Glyph | undefined>;
_cmapProcessor: { lookup(cp: number): number };
_getBaseGlyph(id: number): { _decode?(): { components?: Array<{ glyphID: number }> } | null };
};
const bad: string[] = [];
let checked = 0;
for (const fam of FONT_MANIFEST.families) {
for (const s of familySources(fam)) {
await Font.load({ fontFamily: fam.family, fontWeight: s.fontWeight, fontStyle: s.fontStyle });
const font = fontOf(fam.family, s.fontWeight, s.fontStyle);
const peek = font as unknown as Peek;
const mapped = new Set(font.characterSet.filter((cp) => cp < 0xe000 || cp > 0xf8ff).map((cp) => peek._cmapProcessor.lookup(cp)));
const comps = new Set<number>();
const cache = peek._glyphs;
peek._glyphs = {};
for (let id = 0; id < peek.numGlyphs; id++) for (const c of peek._getBaseGlyph(id)._decode?.()?.components ?? []) comps.add(c.glyphID);
peek._glyphs = cache;
for (const id of comps) {
if (!mapped.has(id)) continue;
checked++;
if ((peek._glyphs[id]?.codePoints.length ?? 0) === 0) bad.push(`${fam.family} ${s.file} component glyph ${id}`);
}
}
}
expect(checked).toBeGreaterThan(1000);
expect(bad).toEqual([]);
});
});