Repository navigation
Description
Summary
On Linux, if fontconfig (the library that tells programs which fonts are installed) lists zero fonts, hyperframes render exits 0 and every frame has no text. This happens even though HyperFrames embeds a web font for the text. The log looks like a good render, and check, validate, snapshot and doctor all pass. This can happen in a slim container with no font packages, or when fontconfig can't load its config and its fallback font folder is missing (see Environment).
How to reproduce
opaque/index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
html, body { margin: 0; background: #202020; }
#root { position: relative; width: 1080px; height: 1920px; overflow: hidden; background: #202020; }
#t { position: absolute; left: 0; right: 0; top: 880px; margin: 0; text-align: center;
font-family: Arial, sans-serif; font-size: 120px; font-weight: 700; color: #fff; }
</style>
</head>
<body>
<div id="root" data-composition-id="root" data-width="1080" data-height="1920" data-fps="30" data-duration="3" data-no-timeline>
<p id="t" class="clip" data-start="0" data-duration="3">Hello world</p>
</div>
</body>
</html>- Copy it to
alpha/index.htmlwith bothbackground: #202020set totransparent. - Render in HyperFrames' own
--dockerimage with the system fonts moved away:
docker run --rm -v "$PWD:/blank" --entrypoint sh hyperframes-renderer:0.9.20-arm64 -c '
export HYPERFRAMES_NO_TELEMETRY=1 HYPERFRAMES_NO_UPDATE_CHECK=1
mv /usr/share/fonts /usr/share/fonts.off # fontconfig now has 0 fonts (fc-list | wc -l -> 0)
hyperframes render /blank/opaque --format mp4 --output /blank/out/nofonts.mp4; echo "exit=$?"
hyperframes render /blank/alpha --format mov --output /blank/out/nofonts.mov; echo "exit=$?"'- Run it again without the
mvline as a control. - Measure the brightest pixel in a band across the text, and the alpha plane of the MOV:
ffmpeg -i out/nofonts.mp4 -vf "crop=1080:200:0:860,signalstats,metadata=print:key=lavfi.signalstats.YMAX:file=-" -f null -
ffmpeg -i out/nofonts.mov -vf "alphaextract,signalstats,metadata=print:key=lavfi.signalstats.YMAX:file=-" -f null -What happens / What should happen
Every render exits 0 with 90 frames.
| Case | fc-list | MP4 text band, max luma | MOV alpha max (12-bit; 256 = transparent) |
|---|---|---|---|
| Control (all fonts) | 1909 fonts | 238 in all frames | 3773 in all frames |
FONTCONFIG_FILE=/nonexistent, or /etc/fonts moved |
1909 (fallback config still finds /usr/share/fonts) |
same file as control | same file as control |
/usr/share/fonts moved |
0 | 43 (= background) in all frames | 256 in all frames |
same, plus FONTCONFIG_FILE=/nonexistent |
0 | same file as the row above | not run |
same, with font-family: "Inter" (the embedded family) |
0 | 43 in all frames | not run |
- The no-fonts log matches the control's, apart from one line in a different place. Both say
Fetched 11 font face(s) for "Inter" from Google FontsandInjected deterministic @font-face rules for 1 requested font families. Nothing mentions missing fonts. WithFONTCONFIG_FILE=/nonexistentthe log has noFontconfig errorline, thoughfc-listprints one in the same container. check: exit 0,Check passed,5/5 text checks pass WCAG AA. Its only difference from the control is the Craft warningfirst_frame_empty: Frame 0 shows only the background.validate: exit 0,No console errors · 5 text elements pass WCAG AA.snapshot --at 1.5: exit 0, PNG blank (max luma 43, control 235).- Plain
chromium --headless --screenshotof a page with Arial text is also blank with no fonts. So the missing text comes from Chromium; the silent pass is the HyperFrames part. - With one system font, unrelated to the page's font, the output is byte-identical to the control. Our guess: Chromium draws no text at all, web fonts included, when fontconfig has zero fonts, and otherwise the text comes from the embedded web font.
Should: render (and check/snapshot) fail, or at least warn clearly, when the browser has no usable fonts, instead of exiting 0 with a video that has no text.
Where in the code
packages/cli/src/browser/preflight.ts,runEnvironmentChecks()/checkChrome()/chromeLaunchOutcome(): the preflight shared byrenderanddoctor. On Linux it already probes Chrome's shared libraries, but nothing checks fonts. A source search finds nofontconfigorfc-listcheck outside tests.packages/cli/src/commands/render.ts,executeLocalRender(): stops the render on any failed preflight outcome (errorBox+failCommand).packages/cli/src/commands/doctor.ts: lists the same outcomes.checkandsnapshotdon't callrunEnvironmentChecks(), so a preflight alone won't cover them.packages/core/src/fonts/deterministicFonts.ts: the font fetch and@font-faceinjection. Works; not the cause.packages/cli/src/utils/craftAudit.ts,detectFirstFrameEmpty(): the only signal today (first_frame_empty).
Suggested fix
- Linux preflight: in
runEnvironmentChecks(), count fonts withfc-listbefore launching the browser. Zero fonts fails with the workaround below as the hint;doctorshows "fontconfig fonts: N". Guess: thefc-liston PATH can be a different fontconfig build than the one Chromium loads (as on Nix), so a missingfc-listshould warn, not fail. - In-page canary: after fonts load, draw a short text run with the embedded font and fail if it has no ink (no non-background pixels). This also covers
checkandsnapshot.
Done when
- With
/usr/share/fontsmoved away in the image above,renderexits non-zero (or prints a clear warning, if maintainers prefer) naming missing fonts and the fix. -
checkandsnapshotreport the same problem. - On Linux,
doctorshows the fontconfig font count. - With one font or all fonts, and on macOS and Windows, behaviour is unchanged.
- Regression test in
packages/cli/src/browser/preflight.test.ts: stubbedfc-listoutput with 0 lines gives a failed outcome, N lines gives ok, and a missingfc-listdoesn't fail the run.
Workaround
Give fontconfig one font; the output is then byte-identical to the control:
mkdir -p /tmp/onefont && cp DejaVuSans.ttf /tmp/onefont/
printf '<?xml version="1.0"?>\n<fontconfig><dir>/tmp/onefont</dir></fontconfig>\n' > /tmp/fonts.conf
export FONTCONFIG_FILE=/tmp/fonts.confOn Nix, FONTCONFIG_FILE=${pkgs.makeFontsConf { fontDirectories = [ ]; }} works, because makeFontsConf always adds dejavu_fonts.minimal.
Environment
hyperframes0.9.20 CLI in its own--dockerimagehyperframes-renderer:0.9.20-arm64(node 22.23.4, Debian bookworm, fontconfig 2.14.1)- Debian chromium 156.0.8078.79 (
/usr/bin/chromium, the image's default); Playwright'schrome-headless-shellin the same image gives the same result - Host: macOS on Apple silicon with OrbStack, linux/arm64 container
- First seen with Nix's chromium 156.0.8078.79 in a
nixos/nixcontainer (aarch64): a 66 s transparent MOV caption overlay with--workers 5exited 0 with alpha 256 in all 1986 frames, with both Nixchromiumandchrome-headless-shell. The only hint wasFontconfig error: Cannot load default config filein the terminal. Our guess: Nix's fontconfig had no config there, and its built-in fallback font folder is a store path that wasn't present, so it had zero fonts.
Related
- None known.
Activity
docs: keep HyperFrames 0.8.36, add font and install notes