the effing blog

Effing 0.39.0: a fitting release

findLargestUsableFontSize() gets a whiteSpace option for single-line fitting, plus a batch of hardening fixes.

Effing 0.39.0 is out. The headline is a new whiteSpace option on findLargestUsableFontSize(), the @effing/canvas function that finds the largest font size that fits a piece of text into a box. It’s been part of the package since 0.24.0, but it’s the kind of function you only find when you go looking for it — so let’s start with what it does.

The problem it solves

When a video template renders fixed text, you pick a font size once, eyeball it, and move on. Dynamic text is another matter. A caption typed by a user, a product name from a feed, a localized string that comes back twice as long in German: pick a size that works for the short case and the long case overflows the frame, pick one for the long case and the short one looks timid.

What you want in that situation is the answer to a concrete question: given this box, this font, and this text, what’s the biggest font size that still fits? That’s what findLargestUsableFontSize() computes:

import { findLargestUsableFontSize } from "@effing/canvas";

const fontSize = findLargestUsableFontSize({
  text: caption,
  font: impact,
  maxWidth: 1000,
  maxHeight: 240,
  lineHeight: 1.05,
});

Under the hood it binary-searches over integer font sizes, measuring each candidate with the same text layout engine that renders your JSX. That’s the reason to use it instead of approximating with your own canvas measurements: the wrapping and line-height math are exactly what the renderer will do, so if it says 63 fits, 63 fits.

A few things worth knowing:

  • Set maxFontSize as a cap. Without one, a caption like “OK” balloons to fill the whole box. Cap it at whatever your design considers a sensible maximum.
  • minFontSize is the floor. If even the smallest size overflows, you get minFontSize back and the text clips.
  • It’s a pure measurement. Call it once per render, outside the JSX, and pass the result in as a prop. It costs nothing per frame.

For a real-world example, look at the meme generator in effing-examples. It fits the top and bottom captions independently and takes the minimum of the two results, so both render at a matching size as meme convention demands:

const fontSize = Math.min(...captions.map((text) =>
  findLargestUsableFontSize({
    text: text.toUpperCase(),
    font,
    maxWidth: captionMaxWidth,
    maxHeight: captionMaxHeight,
    lineHeight: 1.05,
    maxFontSize: fontSizeCap,
  }),
));

New in 0.39.0: single-line fitting

By default, findLargestUsableFontSize() wraps text to maxWidth and fits the resulting block into the maxWidth × maxHeight box. For captions and paragraphs that’s the right behavior. But some text has to stay on one line — a lower-third name badge, a scoreboard label, a headline that shouldn’t break — and there wrapping gets in the way: a larger font can “fit” by spilling onto a second line, and as long as the total height still clears maxHeight, the search accepts it. Your one-liner comes back as two.

The new whiteSpace option mirrors the CSS property and is forwarded into the layout engine the same way lineHeight already is. Pass "nowrap" and the text is fit on a single line, with maxWidth constraining the full line width:

const fontSize = findLargestUsableFontSize({
  text: guestName,
  font: nunitoSansBold,
  maxWidth: 640,
  maxHeight: 96,
  whiteSpace: "nowrap",
});

"pre" works too: hard newlines you wrote are respected, and each newline-separated paragraph is fit on its own line. The default is "normal", plain wrapping, so existing callers are unaffected. Just remember to put the same whiteSpace value on the element that renders the text, so measurement and rendering agree.

Also in this release

The rest of 0.39.0 is fixes and hardening, mostly in @effing/ffs, the FFmpeg render service:

  • Background colors are validated. The color background used to accept any string and interpolate it straight into an ffmpeg filtergraph; it’s now constrained to the color forms ffmpeg actually accepts, enforced in both the schema and the renderer.
  • Tighter request security. Outbound fetches are pinned to the DNS results that passed SSRF validation (closing a rebinding window), and both the FFS API-key check and @effing/serde signature verification now use constant-time comparisons.
  • Verified FFmpeg downloads. @effing/ffmpeg checks the SHA-256 of the downloaded binary against pinned per-platform digests.
  • Bounded memory, fewer leaks. Rendered videos stream to a temp file instead of buffering the whole MP4 in memory, connection pools are reused across fetches, and an aborted direct-stream download now cleans up its ffmpeg process instead of leaking it. Renders that hit an untransformable animation frame fail fast instead of hanging.
  • Correctness fixes. imageResponse() in @effing/fn no longer serves the entire backing buffer for offset Buffer views, and partitioned renders now match the monolithic renderer’s background timeline under xfade transitions.
  • A saner concurrency default. @effing/tween caps default concurrency at min(availableParallelism, 8) — beyond that, benchmarks showed extra slots added peak memory without adding throughput.

Upgrading

Bump your @effing/* packages to 0.39.0. It’s a backward-compatible upgrade and whiteSpace is additive. The full per-package changelogs are in the release PR.