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
@@ -1,17 +1,8 @@
import { beforeAll, expect, it } from "vitest";
import { prefsFor, renderFixture } from "./harness";
import { expect, it } from "vitest";
import { describeSweep, STANDALONE_SWEEP } from "./harness.sweep";
import { TEMPLATES } from "./registry";
// Monolith, Cobalt Stripe, Marble and Highlighter: the same sweep as the other templates, in a second file so the two run side by side.
//
// Poppins Regular is warmed with a document that has amounts first. fontkit caches glyph objects per font for the process and
// pdfkit creates a composite's components (the period inside Poppins' colon) at embed time with no code points: Monolith, which
// sets ':' in Poppins Regular but amounts in Medium, would leave the period unextractable for Highlighter's amounts
// (src/pdf/quirks.test.tsx, quirk 13).
beforeAll(async () => {
await renderFixture({ name: "warm-up", items: 3 }, prefsFor("A4", { templateId: "highlighter" }));
});
describeSweep((id) => STANDALONE_SWEEP.includes(id));
@@ -1,6 +1,6 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { CLOSING_ID, TABLE_HEAD_ID, TABLE_ROW_PREFIX, TOTALS_ID } from "../../engine/ids";
import { walk, type LayoutNode } from "../../engine/layoutTree";
import { SLOT_ID } from "../../blocks/slots";
@@ -21,15 +21,6 @@ import { BLACK, HIGHLIGHTER_ROLES, HIGHLIGHTER_VARIANT, INDIGO, INK, YELLOW } fr
ensureSetup();
// fontkit caches glyph objects per font for the process, and pdfkit creates a composite's components (the period and comma inside
// Poppins' colon and semicolon) at embed time with no code points (src/pdf/quirks.test.tsx, quirk 13). A first render without any
// period or comma, such as the 3-row sample at 100.00 an hour, then makes a later wrapped client name overflow its box by 3.6 pt
// (textkit's glyph-to-string mapping slips). The tests run in a fixed order, so warm Poppins with the harness's default invoice,
// whose amounts have both.
beforeAll(async () => {
await renderFixture({ name: "warm-up", items: 3 }, prefsFor("A4", { templateId: "highlighter" }));
});
const V = HIGHLIGHTER_VARIANT;
const slot = OPTIN_SLOTS.highlighter!;
type R = Awaited<ReturnType<typeof renderFixture>>;