Case study D
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.
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.
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.
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.
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.
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.
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.
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.
| Rule | The failure it replaced |
|---|---|
| Figures fade at the base and both sides, via two masks composited to an intersection | The 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 pass | Past that, the resampler starts inventing detail that was never in the photograph. |
| No photograph ever sits behind type | An 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 block | A 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. |
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.
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.
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.
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.
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.
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.