/** * Prepares the ITA ITO brand assets. * * npm run brand * * Downloads the shop logo once and writes two files into `public/brand`, both * of which are committed: * * logo.png the original, for the admin interface * logo-eink.png the wordmark alone, 1-bit and thresholded, for the panel * * The outputs are committed on purpose. Rendering must not depend on a * Shopify CDN being reachable, and the panel image feeds a content hash that * decides whether the screen redraws — so the bytes have to be stable. * * `sharp` is a devDependency used only here. Nothing at runtime needs it. */ import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import sharp from 'sharp'; import { encodePng, toBitmap } from '../lib/screen/encode'; const LOGO_URL = process.env.BRAND_LOGO_URL ?? 'https://ita-ito.com/cdn/shop/files/ITA_ITO_knitwear_logo.png'; /** Wide enough to read from the pavement, short enough for a 76px header. */ const EINK_WIDTH = 260; async function main(): Promise { const directory = path.join(process.cwd(), 'public', 'brand'); await mkdir(directory, { recursive: true }); const response = await fetch(LOGO_URL); if (!response.ok) { throw new Error(`Could not fetch the logo: ${response.status} ${response.statusText}`); } const original = Buffer.from(await response.arrayBuffer()); await writeFile(path.join(directory, 'logo.png'), original); const band = await findWordmarkBand(original); const cropped = await sharp(original) .extract({ left: 0, top: band.top, width: band.width, height: band.height }) .resize({ width: EINK_WIDTH }) .flatten({ background: '#ffffff' }) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); // Reuse the panel's own encoder, so the logo is thresholded by exactly the // rule the rest of the screen is. A line drawing gets no dithering: it would // only fuzz the serifs. const bitmap = toBitmap(cropped.data, cropped.info.width, cropped.info.height); await writeFile(path.join(directory, 'logo-eink.png'), encodePng(bitmap)); console.log(`logo.png ${original.length} bytes (original)`); console.log(`logo-eink.png ${cropped.info.width}x${cropped.info.height}, 1-bit`); console.log('Both are committed; the CDN is never read at runtime.'); } /** * The logo stacks a wordmark, a two-line tagline and a drawn motif. Only the * wordmark survives at panel size, so find its band rather than hard-coding * pixel offsets that a future logo revision would silently break. */ async function findWordmarkBand( source: Buffer, ): Promise<{ top: number; height: number; width: number }> { const { data, info } = await sharp(source) .flatten({ background: '#ffffff' }) .greyscale() .raw() .toBuffer({ resolveWithObject: true }); const bands: { top: number; bottom: number }[] = []; let start: number | null = null; for (let y = 0; y < info.height; y += 1) { let ink = 0; for (let x = 0; x < info.width; x += 1) { if ((data[y * info.width + x] ?? 255) < 128) { ink += 1; } } if (ink > 0 && start === null) { start = y; } else if (ink === 0 && start !== null) { bands.push({ top: start, bottom: y - 1 }); start = null; } } if (start !== null) { bands.push({ top: start, bottom: info.height - 1 }); } const wordmark = bands[0]; if (!wordmark) { throw new Error('Found no ink in the logo; is the download correct?'); } // A couple of pixels of air, so the thresholding cannot clip a serif. const top = Math.max(0, wordmark.top - 2); const bottom = Math.min(info.height - 1, wordmark.bottom + 2); return { top, height: bottom - top + 1, width: info.width }; } main().catch((error: unknown) => { console.error(error); process.exitCode = 1; });