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:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user