diff --git a/src/core/loaders/font-load-parser.ts b/src/core/loaders/font-load-parser.ts index 4061abc0..72eee144 100644 --- a/src/core/loaders/font-load-parser.ts +++ b/src/core/loaders/font-load-parser.ts @@ -6,6 +6,23 @@ type Woff2Decompressor = { onRuntimeInitialized: (value: unknown) => void; }; +/** + * opentype.js 1.3.5 splits the naming table by platform (`names.windows.fontFamily`, + * `names.macintosh.fontFamily`); the flat `names.fontFamily` its types describe isn't + * always present, so a Windows-only font would otherwise crash the parse. + */ +export function readFamilyName(font: opentype.Font): string { + // Cast past @types/opentype.js, which only knows the flat shape. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const names = font.names as any; + const family = names.fontFamily ?? names.windows?.fontFamily ?? names.macintosh?.fontFamily ?? {}; + const familyName = family.en ?? family[Object.keys(family)[0]]; + if (!familyName) { + throw new Error(`Font has no readable family name; name tables present: ${Object.keys(names).join(", ") || "none"}`); + } + return familyName; +} + export class FontLoadParser implements pixi.LoaderParser { public static readonly Name = "FontLoadParser"; @@ -39,7 +56,7 @@ export class FontLoadParser implements pixi.LoaderParser { if (extension !== "woff2") { const font = opentype.parse(new Uint8Array(buffer).buffer); - const familyName = font.names.fontFamily["en"] || font.names.fontFamily[Object.keys(font.names.fontFamily)[0]]; + const familyName = readFamilyName(font); const fontFace = new FontFace(familyName, `url(${url})`); await fontFace.load(); @@ -56,7 +73,7 @@ export class FontLoadParser implements pixi.LoaderParser { const decompressed = this.woff2Decompressor.decompress(buffer); const font = opentype.parse(new Uint8Array(decompressed).buffer); - const familyName = font.names.fontFamily["en"] || font.names.fontFamily[Object.keys(font.names.fontFamily)[0]]; + const familyName = readFamilyName(font); const blob = new Blob([decompressed], { type: "font/ttf" }); const blobUrl = URL.createObjectURL(blob); diff --git a/tests/font-load-parser.test.ts b/tests/font-load-parser.test.ts new file mode 100644 index 00000000..0942b6ca --- /dev/null +++ b/tests/font-load-parser.test.ts @@ -0,0 +1,36 @@ +/** + * opentype.js 1.3.5 splits the naming table by platform, so a Windows-only font + * used to crash the loader reading the flat `names.fontFamily` its types describe. + */ +import { readFamilyName } from "@loaders/font-load-parser"; + +jest.mock("pixi.js", () => ({})); + +// Synthetic parse result — only the `names` field readFamilyName touches. +const font = (names: unknown) => ({ names }) as unknown as Parameters[0]; + +describe("readFamilyName — opentype.js platform-split name tables", () => { + it("reads a Windows-only name table (the regression)", () => { + expect(readFamilyName(font({ windows: { fontFamily: { en: "Roboto" } } }))).toBe("Roboto"); + }); + + it("reads a Macintosh-only name table", () => { + expect(readFamilyName(font({ macintosh: { fontFamily: { en: "Helvetica Neue" } } }))).toBe("Helvetica Neue"); + }); + + it("reads the flat name table older opentype.js versions exposed", () => { + expect(readFamilyName(font({ fontFamily: { en: "Open Sans" } }))).toBe("Open Sans"); + }); + + it("prefers Windows over Macintosh when both are present", () => { + expect(readFamilyName(font({ windows: { fontFamily: { en: "Win" } }, macintosh: { fontFamily: { en: "Mac" } } }))).toBe("Win"); + }); + + it("falls back to the first locale when there is no English entry", () => { + expect(readFamilyName(font({ windows: { fontFamily: { ja: "ヒラギノ" } } }))).toBe("ヒラギノ"); + }); + + it("throws a debuggable error when no platform table carries a family name", () => { + expect(() => readFamilyName(font({ windows: {}, macintosh: {} }))).toThrow(/no readable family name.*windows, macintosh/); + }); +});