Skip to content

render exits 0 with all text missing when Chromium finds no system fonts on Linux #7406

Open

Description

@guillempuche

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>
  1. Copy it to alpha/index.html with both background: #202020 set to transparent.
  2. Render in HyperFrames' own --docker image 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=$?"'
  1. Run it again without the mv line as a control.
  2. 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 Fonts and Injected deterministic @font-face rules for 1 requested font families. Nothing mentions missing fonts. With FONTCONFIG_FILE=/nonexistent the log has no Fontconfig error line, though fc-list prints 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 warning first_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 --screenshot of 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 by render and doctor. On Linux it already probes Chrome's shared libraries, but nothing checks fonts. A source search finds no fontconfig or fc-list check 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. check and snapshot don't call runEnvironmentChecks(), so a preflight alone won't cover them.
  • packages/core/src/fonts/deterministicFonts.ts: the font fetch and @font-face injection. Works; not the cause.
  • packages/cli/src/utils/craftAudit.ts, detectFirstFrameEmpty(): the only signal today (first_frame_empty).

Suggested fix

  1. Linux preflight: in runEnvironmentChecks(), count fonts with fc-list before launching the browser. Zero fonts fails with the workaround below as the hint; doctor shows "fontconfig fonts: N". Guess: the fc-list on PATH can be a different fontconfig build than the one Chromium loads (as on Nix), so a missing fc-list should warn, not fail.
  2. 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 check and snapshot.

Done when

  • With /usr/share/fonts moved away in the image above, render exits non-zero (or prints a clear warning, if maintainers prefer) naming missing fonts and the fix.
  • check and snapshot report the same problem.
  • On Linux, doctor shows 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: stubbed fc-list output with 0 lines gives a failed outcome, N lines gives ok, and a missing fc-list doesn'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.conf

On Nix, FONTCONFIG_FILE=${pkgs.makeFontsConf { fontDirectories = [ ]; }} works, because makeFontsConf always adds dejavu_fonts.minimal.

Environment

  • hyperframes 0.9.20 CLI in its own --docker image hyperframes-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's chrome-headless-shell in 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/nix container (aarch64): a 66 s transparent MOV caption overlay with --workers 5 exited 0 with alpha 256 in all 1986 frames, with both Nix chromium and chrome-headless-shell. The only hint was Fontconfig error: Cannot load default config file in 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

  1. added
    bugSomething isn't working
    triage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.
    on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriage/needs-designEvidence is adequate; desired behavior or scope needs a maintainer decision.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions