diff --git a/packages/core/src/fonts/fontCompression.test.ts b/packages/core/src/fonts/fontCompression.test.ts index 4c20358a0d..75b6aabef8 100644 --- a/packages/core/src/fonts/fontCompression.test.ts +++ b/packages/core/src/fonts/fontCompression.test.ts @@ -136,6 +136,25 @@ describe("fontToDataUri", () => { expect(uri).toMatch(/^data:font\/otf;base64,/); }); + it("embeds ttf and otf raw without compressing or caching when compress is false", async () => { + const cacheDir = mkdtempSync(join(tmpdir(), "hf-font-compression-off-")); + const compressImpl = vi.fn(async () => Buffer.from("woff2")); + try { + const raw = Buffer.from("font-bytes"); + const options = { cacheDir, compressImpl, compressToWoff2: false }; + expect(await fontToDataUri(raw, "ttf", options)).toBe( + `data:font/ttf;base64,${raw.toString("base64")}`, + ); + expect(await fontToDataUri(raw, "otf", options)).toBe( + `data:font/otf;base64,${raw.toString("base64")}`, + ); + expect(compressImpl).not.toHaveBeenCalled(); + expect(readdirSync(cacheDir)).toEqual([]); + } finally { + rmSync(cacheDir, { recursive: true, force: true }); + } + }); + it("embeds ttc collections raw without attempting slow woff2 compression", async () => { const warnings: unknown[][] = []; const originalWarn = console.warn; diff --git a/packages/core/src/fonts/fontCompression.ts b/packages/core/src/fonts/fontCompression.ts index 1578aae98c..bbeee94455 100644 --- a/packages/core/src/fonts/fontCompression.ts +++ b/packages/core/src/fonts/fontCompression.ts @@ -33,6 +33,7 @@ function rawMimeType(format: string): string { type FontCompressionOptions = { cacheDir?: string; compressImpl?: (input: Buffer) => Promise; + compressToWoff2?: boolean; }; function defaultCacheDir(): string { @@ -94,8 +95,8 @@ export async function fontToDataUri( if (originalFormat === "woff2") { return `data:font/woff2;base64,${input.toString("base64")}`; } - if (originalFormat === "ttc") { - return `data:font/collection;base64,${input.toString("base64")}`; + if (originalFormat === "ttc" || options.compressToWoff2 === false) { + return `data:${rawMimeType(originalFormat)};base64,${input.toString("base64")}`; } try { const cachePath = cachedCompressionPath( diff --git a/packages/producer/src/services/htmlCompiler.test.ts b/packages/producer/src/services/htmlCompiler.test.ts index 85050835b8..8ba7639699 100644 --- a/packages/producer/src/services/htmlCompiler.test.ts +++ b/packages/producer/src/services/htmlCompiler.test.ts @@ -1,7 +1,7 @@ // fallow-ignore-file code-duplication import { describe, expect, it, mock, beforeAll } from "bun:test"; import { createHash } from "node:crypto"; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync, readdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runInThisContext } from "node:vm"; @@ -1211,6 +1211,52 @@ describe("local font embedding", () => { expect(embeddedMessages).toHaveLength(1); }); + it("embeds local ttf and otf fonts as-is without woff2 compression", async () => { + const projectDir = mkdtempSync(join(tmpdir(), "hf-local-font-raw-")); + const cacheDir = mkdtempSync(join(tmpdir(), "hf-local-font-cache-")); + const font = readFileSync( + join( + import.meta.dirname, + "../../../../docs/public/catalog/items/frost-sequence-camera-orbit/assets/fonts/Geist-Regular.ttf", + ), + ); + writeFileSync(join(projectDir, "a.ttf"), font); + writeFileSync(join(projectDir, "b.otf"), font); + writeFileSync( + join(projectDir, "index.html"), + ` + +
Text
+`, + ); + + const previousCacheDir = process.env.HYPERFRAMES_FONT_CACHE_DIR; + process.env.HYPERFRAMES_FONT_CACHE_DIR = cacheDir; + let html: string; + try { + ({ html } = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir)); + } finally { + if (previousCacheDir === undefined) delete process.env.HYPERFRAMES_FONT_CACHE_DIR; + else process.env.HYPERFRAMES_FONT_CACHE_DIR = previousCacheDir; + } + + const embedded = (mime: string) => + Buffer.from( + html.match(new RegExp(`data:${mime};base64,([A-Za-z0-9+/=]+)`))?.[1] ?? "", + "base64", + ); + expect(embedded("font/ttf").equals(font)).toBe(true); + expect(embedded("font/otf").equals(font)).toBe(true); + expect(html).not.toContain("data:font/woff2"); + // The woff2 compressor writes every result to this cache; empty means it never ran. + expect(readdirSync(cacheDir, { recursive: true })).toEqual([]); + rmSync(projectDir, { recursive: true, force: true }); + rmSync(cacheDir, { recursive: true, force: true }); + }); + it("keeps large local font collections file-backed instead of expanding them into HTML", async () => { const projectDir = mkdtempSync(join(tmpdir(), "hf-large-local-font-")); const assetsDir = join(projectDir, "assets"); diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index a810dc4d85..f25739a5fc 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -1841,7 +1841,7 @@ async function embedLocalFontFaces(html: string, projectDir: string): Promise