Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/core/loaders/font-load-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FontFace | null> {
public static readonly Name = "FontLoadParser";

Expand Down Expand Up @@ -39,7 +56,7 @@ export class FontLoadParser implements pixi.LoaderParser<FontFace | null> {

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();
Expand All @@ -56,7 +73,7 @@ export class FontLoadParser implements pixi.LoaderParser<FontFace | null> {
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);
Expand Down
36 changes: 36 additions & 0 deletions tests/font-load-parser.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof readFamilyName>[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/);
});
});
Loading