mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 15:49:53 +00:00
fix(producer): keep large local fonts file-backed (#2864)
* fix(producer): keep large local fonts file-backed * fix(producer): avoid local font file races * fix(producer): bound local font stream reads * fix(producer): cache large font file-backed decisions
This commit is contained in:
@@ -892,6 +892,57 @@ describe("local font embedding", () => {
|
|||||||
|
|
||||||
expect(embeddedMessages).toHaveLength(1);
|
expect(embeddedMessages).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
mkdirSync(assetsDir, { recursive: true });
|
||||||
|
writeFileSync(join(assetsDir, "large.ttc"), Buffer.alloc(6 * 1024 * 1024, 0x41));
|
||||||
|
writeFileSync(
|
||||||
|
join(projectDir, "index.html"),
|
||||||
|
`<!DOCTYPE html>
|
||||||
|
<html><head><style>
|
||||||
|
@font-face {
|
||||||
|
font-family: "LargeLocal";
|
||||||
|
src: url("assets/large.ttc") format("collection");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "LargeLocalAlias";
|
||||||
|
src: url("./assets/large.ttc") format("collection");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "LargeLocalNormalized";
|
||||||
|
src: url("assets/../assets/large.ttc") format("collection");
|
||||||
|
}
|
||||||
|
</style></head><body>
|
||||||
|
<div data-composition-id="root" data-width="640" data-height="360" data-duration="1">
|
||||||
|
Text
|
||||||
|
</div>
|
||||||
|
</body></html>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalInfo = defaultLogger.info;
|
||||||
|
const fileBackedMessages: string[] = [];
|
||||||
|
defaultLogger.info = (message) => {
|
||||||
|
if (message.includes("Kept large local font file-backed")) {
|
||||||
|
fileBackedMessages.push(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let compiled: Awaited<ReturnType<typeof compileForRender>>;
|
||||||
|
try {
|
||||||
|
compiled = await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
|
||||||
|
} finally {
|
||||||
|
defaultLogger.info = originalInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(compiled.html).toContain('url("assets/large.ttc")');
|
||||||
|
expect(compiled.html).toContain('url("./assets/large.ttc")');
|
||||||
|
expect(compiled.html).toContain('url("assets/../assets/large.ttc")');
|
||||||
|
expect(compiled.html).not.toContain("data:font/collection;base64,");
|
||||||
|
expect(Buffer.byteLength(compiled.html)).toBeLessThan(1024 * 1024);
|
||||||
|
expect(fileBackedMessages).toHaveLength(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("template-wrapped sub-composition media offsets", () => {
|
describe("template-wrapped sub-composition media offsets", () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* recursively extracting nested media from sub-sub-compositions.
|
* recursively extracting nested media from sub-sub-compositions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync, mkdirSync } from "fs";
|
import { createReadStream, existsSync, mkdirSync, readFileSync } from "fs";
|
||||||
import { join, dirname, resolve, basename } from "path";
|
import { join, dirname, resolve, basename } from "path";
|
||||||
import { parseHTML } from "linkedom";
|
import { parseHTML } from "linkedom";
|
||||||
import {
|
import {
|
||||||
@@ -1655,6 +1655,28 @@ export async function localizeRemoteFontFaces(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_FONTFACE_URL_RE = /url\(["']?(?!data:|https?:\/\/)([^"')]+)["']?\)/gi;
|
const LOCAL_FONTFACE_URL_RE = /url\(["']?(?!data:|https?:\/\/)([^"')]+)["']?\)/gi;
|
||||||
|
// Base64 expands bytes by ~33%, then immutable HTML replacements retain more
|
||||||
|
// string copies while compiling. Files up to and including 5 MiB remain inline;
|
||||||
|
// the first byte above that stays file-backed. This conservative ceiling keeps
|
||||||
|
// verified 19 MiB+ TTC collections out of the V8 heap while both local and
|
||||||
|
// distributed file servers continue serving project assets at authored paths.
|
||||||
|
const MAX_LOCAL_FONT_DATA_URI_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
|
type LocalFontRead = { kind: "file-backed" } | { kind: "inline"; buffer: Buffer };
|
||||||
|
|
||||||
|
async function readLocalFont(absPath: string): Promise<LocalFontRead> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let totalBytes = 0;
|
||||||
|
for await (const chunk of createReadStream(absPath)) {
|
||||||
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
totalBytes += buffer.length;
|
||||||
|
if (totalBytes > MAX_LOCAL_FONT_DATA_URI_BYTES) {
|
||||||
|
return { kind: "file-backed" };
|
||||||
|
}
|
||||||
|
chunks.push(buffer);
|
||||||
|
}
|
||||||
|
return { kind: "inline", buffer: Buffer.concat(chunks, totalBytes) };
|
||||||
|
}
|
||||||
|
|
||||||
// fallow-ignore-next-line complexity
|
// fallow-ignore-next-line complexity
|
||||||
async function embedLocalFontFaces(html: string, projectDir: string): Promise<string> {
|
async function embedLocalFontFaces(html: string, projectDir: string): Promise<string> {
|
||||||
@@ -1664,6 +1686,7 @@ async function embedLocalFontFaces(html: string, projectDir: string): Promise<st
|
|||||||
let result = html;
|
let result = html;
|
||||||
const embeddedPaths = new Set<string>();
|
const embeddedPaths = new Set<string>();
|
||||||
const dataUriByAbsolutePath = new Map<string, string>();
|
const dataUriByAbsolutePath = new Map<string, string>();
|
||||||
|
const fileBackedAbsolutePaths = new Set<string>();
|
||||||
|
|
||||||
let styleMatch: RegExpExecArray | null;
|
let styleMatch: RegExpExecArray | null;
|
||||||
while ((styleMatch = styleBlockRe.exec(html)) !== null) {
|
while ((styleMatch = styleBlockRe.exec(html)) !== null) {
|
||||||
@@ -1679,16 +1702,27 @@ async function embedLocalFontFaces(html: string, projectDir: string): Promise<st
|
|||||||
if (!localPath || embeddedPaths.has(localPath)) continue;
|
if (!localPath || embeddedPaths.has(localPath)) continue;
|
||||||
const absPath = localPath.startsWith("/") ? localPath : resolve(projectDir, localPath);
|
const absPath = localPath.startsWith("/") ? localPath : resolve(projectDir, localPath);
|
||||||
if (!isPathInside(absPath, projectDir)) continue;
|
if (!isPathInside(absPath, projectDir)) continue;
|
||||||
if (!existsSync(absPath)) continue;
|
|
||||||
const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
|
const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
|
||||||
try {
|
try {
|
||||||
|
if (fileBackedAbsolutePaths.has(absPath)) {
|
||||||
|
embeddedPaths.add(localPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let dataUri = dataUriByAbsolutePath.get(absPath);
|
let dataUri = dataUriByAbsolutePath.get(absPath);
|
||||||
if (!dataUri) {
|
if (!dataUri) {
|
||||||
const buffer = readFileSync(absPath);
|
const font = await readLocalFont(absPath);
|
||||||
dataUri = await toDataUri(buffer, ext);
|
if (font.kind === "file-backed") {
|
||||||
|
fileBackedAbsolutePaths.add(absPath);
|
||||||
|
defaultLogger.info(
|
||||||
|
`[Compiler] Kept large local font file-backed: ${localPath} (> ${(MAX_LOCAL_FONT_DATA_URI_BYTES / 1024 / 1024).toFixed(1)} MB)`,
|
||||||
|
);
|
||||||
|
embeddedPaths.add(localPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dataUri = await toDataUri(font.buffer, ext);
|
||||||
dataUriByAbsolutePath.set(absPath, dataUri);
|
dataUriByAbsolutePath.set(absPath, dataUri);
|
||||||
defaultLogger.info(
|
defaultLogger.info(
|
||||||
`[Compiler] Embedded local font file: ${localPath} (${(buffer.length / 1024).toFixed(0)} KB → data URI)`,
|
`[Compiler] Embedded local font file: ${localPath} (${(font.buffer.length / 1024).toFixed(0)} KB → data URI)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
result = result.replaceAll(localPath, dataUri);
|
result = result.replaceAll(localPath, dataUri);
|
||||||
|
|||||||
Reference in New Issue
Block a user