Skip to content

Commit 94aa82a

Browse files
committed
fix(library): count redrawn pixels in the cover sync check
Averaging the difference diluted a local edit across all 810,000 pixels. Changing a title's "2026" to "2027" moved the mean by 0.42 — under the tolerance that absorbed encoder noise — so the check passed a cover still showing the old year. Counts pixels that moved more than 48 greyscale levels instead. Measured on this cover set, that one-character edit redraws 2,559 pixels while three deliberately different encodes of an identical render (quality 60/70 without mozjpeg, quality 95 with) redraw none, so the count separates real drift from encoder variance in both directions.
1 parent 9cd0152 commit 94aa82a

1 file changed

Lines changed: 31 additions & 20 deletions

File tree

scripts/generate-library-covers.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
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
*/
5151
const 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. */
6272
const 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

Comments
 (0)