const fs = require('node:fs/promises'); const path = require('node:path'); const { createHash } = require('node:crypto'); const sharp = require('sharp'); const esbuild = require('esbuild'); const { textToAscii, POPULAR_FONTS } = require('../lib/text-core.ts'); const { StandardMode } = require('../lib/modes/standard.ts'); const root = path.join(__dirname, '..'); const directory = path.join(root, 'public', 'research', '2026-11-08'); const hash = bytes => createHash('sha256').update(bytes).digest('hex'); const json = value => JSON.stringify(value, null, 2) + '\n'; const baseline = '90dfd52964bf937786a61af2ac30442e78daa051'; function fontMeasurements() { const version = require('../package-lock.json').packages['node_modules/figlet'].version; const phrases = ['HELLO', 'BUILD OK', 'ASCII ART']; const figlet = require('figlet'); const records = POPULAR_FONTS.flatMap(({ value: font }) => phrases.map(text => { const output = textToAscii(text, { font, width: 200, horizontalLayout: 'default', verticalLayout: 'default' }); if (/[^\x20-\x7e\n]/.test(output)) throw new Error(`Non-ASCII banner: ${font}`); const lines = output.split('\n'); if (lines.length > figlet.figFonts[font].options.height) throw new Error(`Wrapped banner: ${font}`); return { font, phrase: text, columns: Math.max(...lines.map(line => line.length)), rows: lines.length, fits80: lines.every(line => line.length <= 80), text: output, sha256: hash(output) }; })); return { schemaVersion: 1, baseline, figletVersion: version, phrases, settings: { width: 200, horizontalLayout: 'default', verticalLayout: 'default' }, method: 'Unwrapped output; maximum ASCII characters per row, including trailing spaces. No terminal emulator measured.', records }; } async function prepare() { await fs.mkdir(directory, { recursive: true }); await fs.writeFile(path.join(directory, 'figlet-widths.json'), json(fontMeasurements())); for (const name of ['gradient', 'checkerboard', 'disk']) { const pixels = Buffer.alloc(320 * 180 * 4); for (let y = 0; y < 180; y++) for (let x = 0; x < 320; x++) { const value = name === 'gradient' ? Math.round(x / 319 * 255) : name === 'checkerboard' ? (Math.floor(x / 20) + Math.floor(y / 20)) % 2 * 255 : ((x - 159.5) ** 2 + (y - 89.5) ** 2 <= 70 ** 2 ? 0 : 255); const i = (y * 320 + x) * 4; pixels[i] = pixels[i + 1] = pixels[i + 2] = value; pixels[i + 3] = 255; } await fs.writeFile(path.join(directory, `${name}.png`), await sharp(pixels, { raw: { width: 320, height: 180, channels: 4 } }).png().toBuffer()); } const script = await fs.readFile(path.join(__dirname, 'research-browser.ts'), 'utf8'); await fs.writeFile(path.join(directory, 'method.txt'), script.replace(/\r\n?/g, '\n')); await fs.writeFile(path.join(directory, 'figlet-method.txt'), (await fs.readFile(__filename, 'utf8')).replace(/\r\n?/g, '\n')); await esbuild.build({ entryPoints: [path.join(__dirname, 'research-browser.ts')], bundle: true, format: 'esm', platform: 'browser', target: 'chrome116', outfile: path.join(directory, 'capture.js') }); const html = '
Local synthetic fixtures only. No analytics, external requests or automatic submission. Canvas results depend on the browser and installed fonts.
Measuring...Download measurements JSON\n'; await fs.writeFile(path.join(directory, 'capture.html'), html); await fs.writeFile(path.join(directory, 'capture-page.txt'), html); } async function importCapture(file) { const capture = JSON.parse(await fs.readFile(file, 'utf8')); if (capture.schemaVersion !== 1 || capture.exports.length !== 9 || capture.fonts.length !== 4) throw new Error('Unexpected capture schema'); for (const item of capture.exports) { const png = Buffer.from(item.png, 'base64'); const rgba = Buffer.from(item.rgba, 'base64'); const source = await fs.readFile(path.join(directory, `${item.source}.png`)); const text = new StandardMode().convertToAscii(new Uint8ClampedArray(rgba), item.columns, item.rows, capture.ramp, capture.options).replace(/\n$/, ''); const metadata = await sharp(png).metadata(); if (text !== item.text || hash(item.text) !== item.txtSha256 || Buffer.byteLength(item.text) !== item.txtBytes || png.length !== item.pngBytes || hash(png) !== item.pngSha256 || hash(rgba) !== item.rasterSha256 || hash(source) !== item.sourceSha256 || metadata.width !== item.pngWidth || metadata.height !== item.pngHeight) throw new Error(`Unverified export: ${item.source}/${item.columns}`); await fs.writeFile(path.join(directory, `${item.source}-${item.columns}.txt`), item.text); await fs.writeFile(path.join(directory, `${item.source}-${item.columns}.png`), png); } const common = { schemaVersion: 1, baseline, capturedAt: capture.capturedAt, userAgent: capture.userAgent, platform: capture.platform, captureSha256: hash(await fs.readFile(path.join(directory, 'capture.js'))) }; await fs.writeFile(path.join(directory, 'export-sizes.json'), json({ ...common, sourceWidth: capture.sourceWidth, sourceHeight: capture.sourceHeight, ramp: capture.ramp, options: capture.options, pngSettings: capture.pngSettings, records: capture.exports.map(({ png: _png, ...record }) => record) })); await fs.writeFile(path.join(directory, 'character-widths.json'), json({ ...common, settings: capture.glyphSettings, fonts: capture.fonts })); await fs.unlink(path.join(directory, 'capture.html')); } if (require.main === module) { const command = process.argv[2]; const job = command === '--prepare' ? prepare() : command === '--import' && process.argv[3] ? importCapture(process.argv[3]) : Promise.reject(new Error('Use --prepare or --import