← Portfolio

Case study D

Built to need no tuning

A publishing pipeline that has to produce dozens of composed images a day, to a standard, without a designer touching any of them. Five passes were rejected before it worked. Every constant in the finished system is there because it replaced a specific failure.

Role
Sole builder
Duration
2026
Throughput
~1 min compute + 5 min data entry per piece
Stack
Node, headless rendering, alpha/pixel analysis, CSS masks
01

The problem

Every day, one editorial input becomes a set of published pieces, and every piece needs composed artwork at three sizes. The subjects change daily and the source photographs are whatever exists — different cameras, different backgrounds, different distances, wildly different quality.

A designer could make any one of these look right in ten minutes. Nobody can do that thirty times before a morning deadline, every day, indefinitely. So the system had to produce a publishable composition from an arbitrary photograph with no human adjustment at all.

Five successive passes were rejected. Not because they looked bad on average — because each one looked wrong on some particular photograph, and there is no such thing as an average photograph in a daily pipeline.

That is the actual difficulty. Any approach works on the well-lit, well-framed picture. The pipeline lives or dies on the one shot against a bright sky, the one cropped to the chest, the one where the subject is holding something.

02

The approach

One governing decision: fix the variables by construction rather than correcting them per item. Every rejected pass had controlled one variable and let another float, then needed a human to fix the drift. If the framing is derived from something measured in the image itself, there is nothing left to tune.

And one discipline that made the system survivable: every rule is recorded with the failure it replaced. The documentation says, in as many words, do not “simplify” these without knowing which failure comes back — because a constant with no story attached looks like arbitrary fussiness to the next person, and gets removed.

03

The method

Frame in head-widths, not in tiles

Figures are framed 2.55 head-widths across and 2.95 down from the crown, centred on the head, padded with transparency where the source runs out.

Three earlier approaches failed here. Filling the tile made head size depend on how much torso the photograph happened to include. Hand-set scales were guesses. Even matching measured head size still looked wrong, because a crop that is almost all head and one that runs to the chest are simply different pictures. Framing in head-widths fixes head size and framing at once, so every output shares one aspect ratio — 0.864 — and one head-to-frame ratio. The layout then needs no per-subject tuning, which was the entire objective.

Measure the head at the 25th percentile, not the median

Head width is taken as a low percentile of row widths across the top third of the alpha outline. The obvious choice is the median, and the median is wrong: people in these photographs are often holding an object that is almost exactly head-width, and the median happily measures the object. Two subjects in one set came back at 428px and 873px for heads that were nearly the same size.

Separate a held object from the person by flood-filling from the face

Objects not touching the subject are removed by filling outward from the face and discarding anything the fill never reaches. The documented exceptions are the interesting part: where a subject's hand is spread across the object, it is deliberately kept — cutting it would cut through the inside of her hand and leave a hole ringed by fingers. Another is handled by the base fade instead. One is painted out by hand and recorded as a manual exception rather than pretended away.

Meter exposure on skin, not on the crop

Brightness is measured over skin pixels only, isolated by colour relationship rather than by brightness, and corrected toward a fixed target. The head box is the wrong thing to meter, because it contains hair and collar — so how dark it reads depends on the subject's hair, not on the exposure. One subject's box read well below target and was lifted; that lift took her actual skin values to the brightest, flattest face in the set.

Saturation is metered the same way against its own target, because bright-and-flat is a different fault from dark, and a brightness gain cannot fix it.

Erode the matte fringe, and allow overrides

Background removal leaves a halo of the old background. It is eroded with a separable minimum filter — and the width is per-subject where the source demands it, because a picture shot against bright sky has a wider, lighter halo than one shot against a studio wall. One gold halo from a yellow wall, one red one, one widened override for a sky shot.

Never cut a hard edge, never invent detail, never put type over a face

RuleThe failure it replaced
Figures fade at the base and both sides, via two masks composited to an intersectionThe frame has to cut across the body somewhere, and a hard horizontal edge there is the single thing that makes a cutout look pasted on.
Nothing is scaled past 2.2× its source, enforced in the layout passPast that, the resampler starts inventing detail that was never in the photograph.
No photograph ever sits behind typeAn earlier pass put a subject's face under a long word and cropped her to hair and eyebrows.
The figure row is sized at render time against the measured bottom of the headline blockA headline is one line on one piece and three on another, and the text fitter may have shrunk either — so no height chosen in advance is correct.

And a state that is a design, not a fallback

Many subjects have no photograph we are entitled to use. The sourcing order is strict — our own consented library first, then supplied directly by the subject or a parent, then nothing — and third-party images of minors are never scraped in without an explicit per-source decision. No likeness is ever generated.

So “nothing” had to be a real outcome. The typographic composition is documented as “a legitimate outcome, not a failure”, and designed as its own layout rather than as the photographic one with a hole in it.

04

The outcome

Adding a piece is one data file. No template is touched. The renderer reads it, writes every graphic, and emits a manifest listing each output with its pixel size and alt text — plus a console report naming every subject with no photograph on file, so the gap is announced rather than discovered.

Roughly one minute of compute and five minutes of data entry per piece. On the day I'd point at: six pieces, 101 subjects, reconciled 101 of 101 against the editorial input with no duplicates, all six drafted, and all eighteen delivered files verified byte-for-byte against the local renders — because “I uploaded it” and “the right file is there” are different claims.

Nothing publishes itself. Everything lands as a draft, and only two named people can publish.

05

What I'd do differently

I copied the pipeline instead of parameterising it

This is the one that would make an engineer wince, and they'd be right. The working directory contains ten numbered renderers and, for the per-run stages, eight build scripts, six parsers and four verifiers — one set copied per run, with the date in the filename.

A date in a filename is a parameter that has escaped into the file system.

It happened the way it always happens: each run needed one small difference, copying was thirty seconds and refactoring was an hour, and the deadline was that morning. The cost isn't disk space, it's that a fix applied to today's copy silently never reaches the other seven. These should be one script taking the run as an argument, and the old ones should be in version control rather than in the folder.

A superseded script can still silently undo the current one

My own notes carry a warning that one older script must not be re-run, because it resets a routing field to a superseded template family — which would quietly send every piece back through the wrong renderer. Everything would still build. It would just be wrong.

I wrote a warning where I should have written a guard, or deleted the script. This is the same mistake as documenting a coordination hazard instead of removing it, which means it is now a pattern and not an incident. A rule I only follow when I remember it isn't a rule.

Per-subject overrides accumulate, and nobody prunes them

The fringe width, the hand-painted exception, the kept object — each was the right call for a specific photograph. But they live as constants keyed to individuals, and nothing ever revisits them. That list only grows, and after a few hundred subjects it is a maintenance burden nobody understands.

Overrides should be data with a reason and a date, reviewed on a cycle — not constants in a script. Better still, each one is a small brief for a rule that doesn't exist yet: a sky-shot halo isn't a property of a person, it's a property of a lighting condition the system should be able to detect.

And the thing I'd defend hardest

Recording, for every rule, the exact failure it replaced. It reads like over-writing documentation right up until someone — including me, months later — decides a constant looks arbitrary and removes it. Five rejected passes are encoded in those notes. Without them the sixth pass is just someone's taste, and taste does not survive a handover.