#!/usr/bin/env node /** * One-shot patch: dither visitor photos before they go on the badge canvas. * * node scripts/dither-badge-photos.mjs # patch * node scripts/dither-badge-photos.mjs --check # report only * * A thermal head has no grey. Drawn straight, a photo gets thresholded by the * plane conversion and most of a face lands on one side of that threshold, so * it prints as a solid black mass. This inserts a Floyd-Steinberg pass at * exactly the size the photo will occupy. * * Idempotent, writes printer.js.bak, refuses to run if the anchor is missing. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const here = path.dirname(fileURLToPath(import.meta.url)); const target = path.resolve(here, '..', 'src', 'printer.js'); const checkOnly = process.argv.includes('--check'); function fail(message) { console.error(`\n FAILED: ${message}\n`); console.error(' Nothing was changed.\n'); process.exit(1); } if (!fs.existsSync(target)) fail(`${target} does not exist. Run this from the repo root.`); let source = fs.readFileSync(target, 'utf8'); const original = source; if (source.includes('ditherPhoto(')) { console.log('\n Already patched — printer.js dithers photos. Nothing to do.\n'); process.exit(0); } const changes = []; /* -- 1. Import ---------------------------------------------------- */ const importAnchor = "import { photoAbsolutePath } from './photos.js';"; if (!source.includes(importAnchor)) fail('could not find the photos.js import'); source = source.replace( importAnchor, `${importAnchor}\nimport { ditherPhoto } from './printing/dither.js';` ); changes.push('added the ditherPhoto import'); /* -- 2. Dither once, at final size, before either draw site ------- */ // Matches the existing line regardless of the exact multipliers, so tweaking // the photo size later doesn't break this patch. const sizeAnchor = /const photoSize = photo \? Math\.round\([^;]+\) : 0;/; const match = source.match(sizeAnchor); if (!match) fail('could not find the photoSize calculation in drawBadge'); const insertion = `${match[0]} // Thermal heads print pure black or nothing, so a photo has to be reduced to // 1-bit before it lands on the canvas — otherwise the plane conversion // thresholds it into a solid blob. Done once here rather than at each draw // site, and at exactly photoSize: rescaling a dithered image resamples the // dot pattern back into greys and undoes the whole thing. // // Skipped on the measuring pass, which never paints. if (photo && photoSize > 0 && startY !== null) { photo = ditherPhoto(photo, photoSize); }`; source = source.replace(sizeAnchor, insertion); changes.push('drawBadge dithers the photo at its final size'); /* -- 3. Sanity check ---------------------------------------------- */ if (!/let photo = null;/.test(source)) { fail('photo is not declared with let — it cannot be reassigned. Patch by hand.'); } console.log('\n Changes to src/printer.js:'); for (const c of changes) console.log(` - ${c}`); if (checkOnly) { console.log('\n --check given, nothing written.\n'); process.exit(0); } if (source === original) fail('nothing actually changed'); fs.writeFileSync(`${target}.bak`, original); fs.writeFileSync(target, source); console.log(`\n Wrote ${target}`); console.log(` Backup at ${target}.bak`); console.log('\n Next: docker compose build && docker compose up -d\n');