The rebuild stamp carried the installed package version until this release, and now carries a digest of the rendering code the package ships. Changing what goes into a stamp changes every stamp, so the first build after upgrading renders every image again.
From here the cost of an upgrade follows what the release changed. A release that leaves the templates, the layout, the measuring, the encoding, the bundled fonts and the drawing libraries alone now produces the stamps a project already has, and its next build skips every image. Four of the eleven releases before this one were in that position and re-rendered everything anyway.
Nothing to change in a config or in any code. Budget one full render, which
--dry-run will count for you first.
Text is measured against the fonts a build actually renders with, instead of being estimated from a per-template fudge factor. Wrapping and fitting change as a result, and so do several pieces of the API.
Every image re-renders on the first build after the upgrade, since the rebuild stamp covers the code that draws them and most of it moved.
A build writes the format format names, so the
function that produces the bytes is named for an image rather than for one of
the four things it may return.
// Before
import { renderSvgToPng } from "@kensio/colophon";
// After
import { renderSvgToImage } from "@kensio/colophon";The arguments and the return type are unchanged, and it still returns PNG unless
format says otherwise.
renderMetaImages returns the same objects with that one field renamed, for the
same reason:
for (const image of await renderMetaImages(props, config)) {
await writeFile(`social-${image.name}.png`, image.bytes); // was image.png
}extensionFor(config.format) is what a build names its own files with, if the
filename should follow the format too.
The rebuild stamp goes into JPEG, WebP and AVIF as well as PNG now, so the two functions that write and read it are named for images rather than for one format. See Rebuilds.
// Before
import { readPngStamp, stampPng } from "@kensio/colophon";
// After
import { readImageStamp, stampImage } from "@kensio/colophon";Nothing else changes: the arguments, the return values and the stamps themselves are the same, so images already on disk are still recognised.
A title too long for its lines used to be wrapped at a fixed size and then cut. It is now shrunk, down to about two thirds of its usual size, and only cut if it still does not fit at that floor.
Nothing to change, but existing images will look different: titles that were losing their last few words now keep them, at a smaller size.
Per-pixel noise is the one thing PNG cannot compress, and film grain took a 1200×1200 image from around 36KB to a little over 1.7MB. That is not a cost worth carrying for a treatment, and no theme ever turned it on.
texture: { type: "grain" } now fails validation with a message saying so.
Every other treatment is a fraction of the size; halftone is the nearest in
look, and Textures lists what each one
costs.
Every treatment's lengths went up by half again, because a share image is looked at somewhere between a third and a sixth of the size it is rendered at and the old defaults were pitched to the full-size picture. Dots are 66px apart rather than 44, ruled lines 42px rather than 28, and so on down the list.
Existing images will look different, though they re-render on the upgrade anyway. A project that wants the old look names the lengths outright:
texture: { type: "dots", size: 5, gap: 44 },SIZE_PRESETS.thumbnail came down from textureScale: 3 to 2 with it, since
what that corrects for is the display size and the base it corrects from moved.
A thumbnail therefore looks exactly as it did.
The code template lays tokens out on a character grid, and this was the number
that said how wide a character is. It is measured from the monospace face now.
Delete it from your config, which otherwise fails validation with a message
saying the same thing. To have the width measured rather than assumed, supply
the face as a file under fonts and name it in
code.fontFamily. Builds that do neither fall back to the 0.6 that was the
default here.
highlightCode no longer reports the longest line in characters, because a
character count is not a width: an ideograph is a full em where a Latin letter
is a little over half of one, so the same count is two different widths. The
code template measures the lines it is going to draw instead, and nothing was
left for the field to say.
Only code calling highlightCode directly has anything to change. A custom
template wanting the width of a line should measure it, with the measure its
TemplateContext carries, rather than count it.
// Before
wrapText(title, estimateCharsPerLine(width, fontSize, 0.58));
// After
wrapText(title, width, (line) => measure(line, { fontFamily, fontSize }));It wraps to a width in pixels rather than to a count of characters, and it
breaks a word that is too wide for a line of its own rather than letting it run
off the image. estimateCharsPerLine has gone with the factors it existed for.
Custom templates receive a fourth field, measure, and need no changes to keep
working. What breaks is code that builds a TemplateContext itself to call a
template directly, which is mostly test code. Build one with createMeasurer:
const config = resolveConfig(userConfig);
const svg = await myTemplate.render({
props,
config,
dimensions,
measure: await createMeasurer(config),
});buildSvg, renderMetaImages and generate all do this for you.
Adding the code template made two small breaking changes.
render now returns string | Promise<string>, and buildSvg is async.
A custom template that returns a string still works unchanged. What needs
updating is any direct call site of buildSvg, which now needs an await.
renderMetaImages and generate were already async, so nothing changes for
code that uses those.
title is no longer required, and walkContent and extractProps no longer
skip a file that declares props without one.
This is what lets a code post describe its image entirely through code and
language, with no heading above the panel.
If your project relied on a titleless props block being ignored, those posts
will now get images. Return undefined from a
props mapper to filter them out instead.