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
maxFontSizeas a cap. Without one, a caption like “OK” balloons to fill the whole box. Cap it at whatever your design considers a sensible maximum. minFontSizeis the floor. If even the smallest size overflows, you getminFontSizeback 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
colorbackground 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/serdesignature verification now use constant-time comparisons. - Verified FFmpeg downloads.
@effing/ffmpegchecks 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/fnno 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/tweencaps default concurrency atmin(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.