#!/usr/bin/env node // gen-synthetic-content.mjs -- Regenerates Category A & B content fixtures as CC0. // // Category A: Deterministically regenerable (QR codes, barcodes, OCR text, PDFs, // synthetic audio/video). // Category B: Replaces copyrighted content (Simpsons GIF, ConvertICO SVG, // Shutterstock stock photo) with project-owned CC0 alternatives. // // All output is provably CC0 (generated by project scripts, no third-party IP). // Usage: node tests/fixtures/gen-synthetic-content.mjs // // Dependencies (resolved from apps/api/node_modules): // sharp, qrcode // System deps on PATH: // ffmpeg import { execSync } from "node:child_process"; import { existsSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const IMAGE_VALID = join(__dirname, "image/valid"); const DOC_VALID = join(__dirname, "document/valid"); const DOC_EDGE = join(__dirname, "document/edge"); const require = createRequire(join(__dirname, "../../apps/api/package.json")); const sharp = require("sharp"); const QRCode = require("qrcode"); const FORCE = process.argv.includes("--force"); // Write file only if it does not already exist (or --force is set). // Committed synthetics must not be silently overwritten because // encoder-version differences can change bytes and break manifest hashes. function writeIfMissing(path, data, label) { if (!FORCE && existsSync(path)) { console.log(` SKIP (exists): ${label || path}`); return false; } writeFileSync(path, data); console.log(` ${label || path}`); return true; } // ── Code 128B barcode encoder ────────────────────────────── // Standard Code 128 bar patterns (11 modules each, stop = 13 modules). // Each string is a binary pattern: 1 = dark bar, 0 = light space. const CODE128 = [ "11011001100", "11001101100", "11001100110", "10010011000", "10010001100", "10001001100", "10011001000", "10011000100", "10001100100", "11001001000", "11001000100", "11000100100", "10110011100", "10011011100", "10011001110", "10111001100", "10011101100", "10011100110", "11001110010", "11001011100", "11001001110", "11011100100", "11001110100", "11101101110", "11101001100", "11100101100", "11100100110", "11101100100", "11100110100", "11100110010", "11011011000", "11011000110", "11000110110", "10100011000", "10001011000", "10001000110", "10110001000", "10001101000", "10001100010", "11010001000", "11000101000", "11000100010", "10110111000", "10110001110", "10001101110", "10111011000", "10111000110", "10001110110", "11101110110", "11010001110", "11000101110", "11011101000", "11011100010", "11011101110", "11101011000", "11101000110", "11100010110", "11101101000", "11101100010", "11100011010", "11101111010", "11001000010", "11110001010", "10100110000", "10100001100", "10010110000", "10010000110", "10000101100", "10000100110", "10110010000", "10110000100", "10011010000", "10011000010", "10000110100", "10000110010", "11000010010", "11001010000", "11110111010", "11000010100", "10001111010", "10100111100", "10010111100", "10010011110", "10111100100", "10011110100", "10011110010", "11110100100", "11110010100", "11110010010", "11011011110", "11011110110", "11110110110", "10101111000", "10100011110", "10001011110", "10111101000", "10111100010", "11110101000", "11110100010", "10111011110", "10111101110", "11101011110", "11110101110", "11010000100", // 103 Start A "11010010000", // 104 Start B "11010011100", // 105 Start C ]; const CODE128_STOP = "1100011101011"; function encodeCode128B(text) { const START_B = 104; const values = [START_B]; let checksum = START_B; for (let i = 0; i < text.length; i++) { const v = text.charCodeAt(i) - 32; values.push(v); checksum += v * (i + 1); } values.push(checksum % 103); let bits = ""; for (const v of values) bits += CODE128[v]; bits += CODE128_STOP; return bits; } function barcodeSvg(text, width, height) { const bits = encodeCode128B(text); const barH = height - 40; const quietZone = 20; const barAreaW = width - 2 * quietZone; const unitW = barAreaW / bits.length; const bars = []; for (let i = 0; i < bits.length; i++) { if (bits[i] === "1") { bars.push( ``, ); } } return ` ${bars.join("\n ")} ${text} `; } // ── Minimal PDF generator ────────────────────────────────── function generatePdf( pageCount, contentForPage = (page) => `BT /F1 24 Tf 72 700 Td (Page ${page}) Tj ET`, ) { const objects = []; let nextObj = 1; const addObj = (content) => { const id = nextObj++; objects.push({ id, content }); return id; }; const catalogId = addObj(null); const pagesId = addObj(null); const fontId = addObj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"); const pageIds = []; for (let i = 1; i <= pageCount; i++) { const stream = contentForPage(i); const sId = addObj(`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`); const pId = addObj( `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 612 792] ` + `/Contents ${sId} 0 R /Resources << /Font << /F1 ${fontId} 0 R >> >> >>`, ); pageIds.push(pId); } objects[0].content = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; objects[1].content = `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageCount} >>`; let pdf = "%PDF-1.7\n%\x81\x81\x81\x81\n\n"; const offsets = []; for (const obj of objects) { offsets.push(pdf.length); pdf += `${obj.id} 0 obj\n${obj.content}\nendobj\n\n`; } const xrefOffset = pdf.length; pdf += `xref\n0 ${objects.length + 1}\n`; pdf += "0000000000 65535 f \n"; for (const off of offsets) { pdf += `${String(off).padStart(10, "0")} 00000 n \n`; } pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\n`; pdf += `startxref\n${xrefOffset}\n%%EOF\n`; return Buffer.from(pdf, "binary"); } const COLORED_BLOCK_STREAM = [ "0.12 0.52 0.72 rg", "72 520 240 65 re f", "BT", "/F1 14 Tf", "1 1 1 rg", "84 562 Td", "(BLOCK LINE ONE) Tj", "0 -24 Td", "(BLOCK LINE TWO) Tj", "ET", ].join("\n"); // ── SVG logo (project-owned, replaces ConvertICO brand) ─── function projectSvg() { return ` SnapOtter Test Fixture CC0 geometric test SVG generated by gen-synthetic-content.mjs TEST FIXTURE CC0 - Generated Asset `; } // ── Multi-face placeholder (replaces Shutterstock photo) ── function multiFaceSvg(w, h) { const cols = 5; const rows = 2; const cw = w / cols; const ch = h / rows; const colors = [ "#D4956A", "#C2885E", "#E8B88A", "#A67853", "#BF9570", "#D9A87C", "#CB9068", "#E5C49E", "#B08060", "#C89878", ]; const shapes = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cx = cw * c + cw / 2; const cy = ch * r + ch / 2; const rx = cw * 0.3; const ry = ch * 0.38; const idx = r * cols + c; shapes.push( ``, ); shapes.push( ``, ); shapes.push( ``, ); } } return ` ${shapes.join("\n ")} `; } // ── ffmpeg helper (all args are hardcoded constants) ─────── function ff(args, label) { try { execSync(`ffmpeg ${args}`, { stdio: "pipe" }); console.log(` ${label}`); } catch (e) { console.error(` FAILED: ${label}`, e.stderr?.toString().slice(-300)); throw e; } } // Like ff(), but skip if the output file already exists (unless --force). function ffIfMissing(outPath, args, label) { if (!FORCE && existsSync(outPath)) { console.log(` SKIP (exists): ${label}`); return; } ff(args, label); } // ── Main ─────────────────────────────────────────────────── // // Scoped outputs: // - image/valid/ : QR codes, barcodes, OCR text, SVG logo, multi-face, animated GIF // - document/valid/: alt-2page.pdf, multipage-6.pdf // // Deliberately EXCLUDED (committed real heroes, not synthetics): // - video/valid/media-30s.mp4 (Big Buck Bunny CC-BY clip) // - audio/valid/media-30s.wav (committed TTS/synthetic, hash-locked) // - video/valid/speech-10s.mp4 (macOS TTS, committed) // - audio/valid/speech-*.{wav,flac,ogg,m4a,aac,opus} (macOS TTS, committed) // - video/valid/hero.{mov,webm,mkv,avi} (Big Buck Bunny CC-BY, committed) async function main() { console.log("Regenerating Category A & B content fixtures..."); console.log( `Mode: ${FORCE ? "FORCE (overwrite all)" : "skip existing files (pass --force to overwrite)"}\n`, ); // ── QR codes (A) ── console.log("QR codes:"); const qrData = "https://snapotter.com"; const qrPng = await QRCode.toBuffer(qrData, { width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" }, }); writeIfMissing(join(IMAGE_VALID, "qr-code.png"), qrPng, "qr-code.png"); const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 }); writeIfMissing(join(IMAGE_VALID, "qr-code.svg"), qrSvg, "qr-code.svg"); const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer(); writeIfMissing(join(IMAGE_VALID, "qr-code.avif"), qrAvif, "qr-code.avif"); // ── Barcodes (A) ── console.log("Barcodes:"); const bcText = "SNAPOTTER-TEST-123"; const bcSvgStr = barcodeSvg(bcText, 699, 152); const bcPng = await sharp(Buffer.from(bcSvgStr)).png().toBuffer(); writeIfMissing(join(IMAGE_VALID, "barcode.png"), bcPng, "barcode.png"); const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer(); writeIfMissing(join(IMAGE_VALID, "barcode.avif"), bcAvif, "barcode.avif"); // ── OCR text images (A) ── console.log("OCR text images:"); const ocrEngSvg = ` The quick brown fox 12345 `; const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer(); writeIfMissing(join(IMAGE_VALID, "ocr-clean.png"), ocrEngPng, "ocr-clean.png"); const jpText = "日本語の表記においては,漢字や仮名だけで" + "なく,ローマ字やアラビア数字,さらに句読" + "点や括弧類などの記述記号を用いる。これら" + "を組み合わせて表す日本語の文書では,表記" + "上における種々の問題がある。"; const chars = [...jpText]; const lineLen = Math.ceil(chars.length / 6); const lines = []; for (let i = 0; i < chars.length; i += lineLen) { lines.push(chars.slice(i, i + lineLen).join("")); } const jpSvg = ` ${lines.map((line, i) => `${line}`).join("\n ")} `; const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer(); writeIfMissing(join(IMAGE_VALID, "ocr-japanese.png"), jpPng, "ocr-japanese.png"); // ── PDFs (A: deterministic generator, safe to overwrite) ── console.log("PDFs:"); writeIfMissing(join(DOC_VALID, "alt-2page.pdf"), generatePdf(2), "alt-2page.pdf"); writeIfMissing(join(DOC_VALID, "multipage-6.pdf"), generatePdf(6), "multipage-6.pdf"); writeIfMissing( join(DOC_EDGE, "colored-block.pdf"), generatePdf(1, () => COLORED_BLOCK_STREAM), "colored-block.pdf", ); // ── SVG logo (B: replaces ConvertICO brand) ── console.log("SVG logo (replacing ConvertICO brand):"); writeIfMissing(join(IMAGE_VALID, "svg-logo.svg"), projectSvg(), "svg-logo.svg"); // ── Multi-face placeholder (B: replaces Shutterstock photo) ── console.log("Multi-face placeholder (replacing Shutterstock stock photo):"); const mfSvg = multiFaceSvg(433, 280); const mfWebp = await sharp(Buffer.from(mfSvg)).webp({ quality: 80 }).toBuffer(); writeIfMissing(join(IMAGE_VALID, "multi-face.webp"), mfWebp, "multi-face.webp"); // ── Animated GIF (B: replaces copyrighted Simpsons clip) ── console.log("Animated GIF (replacing copyrighted Simpsons clip):"); const gifOut = join(IMAGE_VALID, "animated-simpsons.gif"); ffIfMissing( gifOut, `-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`, "animated-simpsons.gif", ); // ── Animated APNG (multi-frame, full alpha) for remove-gif-background ── // The committed file is a 4-frame RGBA APNG. This regenerates a small animated // APNG only if the fixture is missing (bytes will differ from the committed one). console.log("Animated APNG (for remove-gif-background):"); const apngOut = join(IMAGE_VALID, "animated.apng"); ffIfMissing( apngOut, `-f lavfi -i "testsrc=duration=1:size=48x48:rate=4" -pix_fmt rgba -plays 0 -f apng -y "${apngOut}"`, "animated.apng", ); // ── Synthetic audio/video (A) ── // NOTE: media-30s.mp4 and media-30s.wav are no longer generated here. // media-30s.mp4 is now a real Big Buck Bunny CC-BY hero clip (committed). // media-30s.wav is a committed synthetic whose hash is locked in manifest.json. // Regenerating either would clobber the committed content with different bytes. console.log("Synthetic audio/video (metadata-tagged synthetics only):"); const audioTagsOut = join(__dirname, "audio/valid/audio-with-tags.mp3"); ffIfMissing( audioTagsOut, `-f lavfi -i "sine=frequency=440:duration=1.2:sample_rate=8000" ` + `-c:a libmp3lame -b:a 64k -ar 8000 -ac 1 ` + `-metadata title="Test Song" -metadata artist="Test Artist" ` + `-metadata album="Test Album" -metadata date="2026" ` + `-metadata genre="Electronic" -metadata track="1/10" ` + `-y "${audioTagsOut}"`, "audio-with-tags.mp3", ); const videoMetaOut = join(__dirname, "video/valid/video-with-meta.mp4"); ffIfMissing( videoMetaOut, `-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` + `-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` + `-y "${videoMetaOut}"`, "video-with-meta.mp4", ); console.log("\nDone. Run gen-manifest.mjs to re-stamp hashes if any files were regenerated."); } main().catch((e) => { console.error("FATAL:", e); process.exit(1); });