1212 * Covers are derived artifacts, not source of truth: the title in the image
1313 * comes from frontmatter, so editing a post's `title` makes its committed
1414 * cover stale. Every run therefore re-renders from scratch rather than
15- * skipping outputs that already exist. Rendering is deterministic — an
16- * unchanged title re-encodes to identical bytes — so a full run is a no-op in
17- * git for everything that did not actually change.
15+ * skipping outputs that already exist. Rendering is deterministic on a given
16+ * machine, so a full run is a no-op in git for everything that did not
17+ * actually change.
1818 *
1919 * Usage, from the repo root:
2020 * bun run library:covers # re-render every post's cover
@@ -50,13 +50,23 @@ const COVER_HEIGHT = 675
5050 */
5151const JPEG_QUALITY = 82
5252/**
53- * Greyscale mean-absolute-difference above which `--check` calls a committed
54- * cover stale. Measured on this cover set: re-encoding an identical render
55- * with a deliberately different encoder (quality 70, mozjpeg off) moves it
56- * ~0.26, while a one-word title change moves it ~12. A threshold of 2 clears
57- * encoder and platform noise by ~8x and still catches any real drift.
53+ * How far one greyscale pixel must move to count as genuinely redrawn rather
54+ * than re-encoded. Measured across three deliberately different encodes of an
55+ * identical render (quality 60/70 without mozjpeg, quality 95 with), the
56+ * largest single-pixel deviation was 20; 48 clears that by well over 2x.
5857 */
59- const MAX_PIXEL_DIFFERENCE = 2
58+ const COVER_PIXEL_DELTA = 48
59+ /**
60+ * How many redrawn pixels `--check` tolerates before calling a cover stale.
61+ *
62+ * Deliberately a count and not an average. Averaging dilutes a local edit
63+ * across all 810,000 pixels: changing a title's "2026" to "2027" moves the
64+ * mean by only 0.42, which any threshold loose enough to absorb encoder noise
65+ * would wave through. That same edit redraws 2,559 pixels, while the three
66+ * re-encodes above redraw none at all — so a count separates the two cases
67+ * with margin to spare in both directions.
68+ */
69+ const MAX_REDRAWN_PIXELS = 200
6070
6171/** Exact hex from a vector trace of the reference cover template, not an estimate off compressed JPEG pixels. */
6272const INK_COLOR = '#515151'
@@ -215,9 +225,8 @@ async function renderCover(
215225}
216226
217227/**
218- * Mean absolute difference between the committed cover and a freshly rendered
219- * one, over decoded greyscale pixels — `null` if the committed file is not the
220- * expected size.
228+ * Number of greyscale pixels the committed cover draws differently from a
229+ * freshly rendered one — `null` if the committed file is not the expected size.
221230 *
222231 * Deliberately not a byte comparison of the JPEGs. libvips/mozjpeg output is
223232 * not portable across OS and CPU, so identical input can encode to different
@@ -226,16 +235,18 @@ async function renderCover(
226235 * while preserving what the check is actually about: whether the committed
227236 * image still renders the current title.
228237 */
229- async function compareCovers ( committedPath : string , rendered : Buffer ) : Promise < number | null > {
238+ async function countRedrawnPixels ( committedPath : string , rendered : Buffer ) : Promise < number | null > {
230239 const decode = ( input : string | Buffer ) =>
231240 sharp ( input ) . greyscale ( ) . raw ( ) . toBuffer ( { resolveWithObject : true } )
232241
233242 const [ a , b ] = await Promise . all ( [ decode ( committedPath ) , decode ( rendered ) ] )
234243 if ( a . info . width !== b . info . width || a . info . height !== b . info . height ) return null
235244
236- let total = 0
237- for ( let i = 0 ; i < a . data . length ; i ++ ) total += Math . abs ( a . data [ i ] - b . data [ i ] )
238- return total / a . data . length
245+ let redrawn = 0
246+ for ( let i = 0 ; i < a . data . length ; i ++ ) {
247+ if ( Math . abs ( a . data [ i ] - b . data [ i ] ) > COVER_PIXEL_DELTA ) redrawn += 1
248+ }
249+ return redrawn
239250}
240251
241252/**
@@ -298,12 +309,12 @@ async function main() {
298309 stale . push ( `${ slug } — no cover committed` )
299310 continue
300311 }
301- const difference = await compareCovers ( outputPath , cover )
302- if ( difference === null ) {
312+ const redrawn = await countRedrawnPixels ( outputPath , cover )
313+ if ( redrawn === null ) {
303314 stale . push ( `${ slug } — committed cover has unexpected dimensions` )
304- } else if ( difference > MAX_PIXEL_DIFFERENCE ) {
315+ } else if ( redrawn > MAX_REDRAWN_PIXELS ) {
305316 stale . push (
306- `${ slug } — committed cover does not render "${ title } " (difference ${ difference . toFixed ( 2 ) } )`
317+ `${ slug } — committed cover does not render "${ title } " (${ redrawn } pixels differ )`
307318 )
308319 }
309320 continue
0 commit comments