diff --git a/tests/fixtures/content/alt-2page.pdf b/tests/fixtures/content/alt-2page.pdf
index 99de3a50..a776a012 100644
Binary files a/tests/fixtures/content/alt-2page.pdf and b/tests/fixtures/content/alt-2page.pdf differ
diff --git a/tests/fixtures/content/audio-with-tags.mp3 b/tests/fixtures/content/audio-with-tags.mp3
index e3702d86..098ca21c 100644
Binary files a/tests/fixtures/content/audio-with-tags.mp3 and b/tests/fixtures/content/audio-with-tags.mp3 differ
diff --git a/tests/fixtures/content/barcode.avif b/tests/fixtures/content/barcode.avif
index d3947896..7b6af9d7 100644
Binary files a/tests/fixtures/content/barcode.avif and b/tests/fixtures/content/barcode.avif differ
diff --git a/tests/fixtures/content/barcode.png b/tests/fixtures/content/barcode.png
index 73e75e57..5236d2b3 100644
Binary files a/tests/fixtures/content/barcode.png and b/tests/fixtures/content/barcode.png differ
diff --git a/tests/fixtures/content/media-30s.mp4 b/tests/fixtures/content/media-30s.mp4
index b2997074..ce975981 100644
Binary files a/tests/fixtures/content/media-30s.mp4 and b/tests/fixtures/content/media-30s.mp4 differ
diff --git a/tests/fixtures/content/media-30s.wav b/tests/fixtures/content/media-30s.wav
index 704a975e..549ffb73 100644
Binary files a/tests/fixtures/content/media-30s.wav and b/tests/fixtures/content/media-30s.wav differ
diff --git a/tests/fixtures/content/multipage-6.pdf b/tests/fixtures/content/multipage-6.pdf
index cb2e90a0..91806ea2 100644
Binary files a/tests/fixtures/content/multipage-6.pdf and b/tests/fixtures/content/multipage-6.pdf differ
diff --git a/tests/fixtures/content/ocr-clean.png b/tests/fixtures/content/ocr-clean.png
index ad673642..ef84876d 100644
Binary files a/tests/fixtures/content/ocr-clean.png and b/tests/fixtures/content/ocr-clean.png differ
diff --git a/tests/fixtures/content/ocr-japanese.png b/tests/fixtures/content/ocr-japanese.png
index ef042c98..dae2d452 100644
Binary files a/tests/fixtures/content/ocr-japanese.png and b/tests/fixtures/content/ocr-japanese.png differ
diff --git a/tests/fixtures/content/qr-code.avif b/tests/fixtures/content/qr-code.avif
index d199488b..ec6ce48b 100644
Binary files a/tests/fixtures/content/qr-code.avif and b/tests/fixtures/content/qr-code.avif differ
diff --git a/tests/fixtures/content/qr-code.png b/tests/fixtures/content/qr-code.png
index f4e0ae4f..cb03f573 100644
Binary files a/tests/fixtures/content/qr-code.png and b/tests/fixtures/content/qr-code.png differ
diff --git a/tests/fixtures/content/qr-code.svg b/tests/fixtures/content/qr-code.svg
index 3c3dac7d..a89a74f8 100644
--- a/tests/fixtures/content/qr-code.svg
+++ b/tests/fixtures/content/qr-code.svg
@@ -1,3 +1 @@
-
+
diff --git a/tests/fixtures/content/video-with-meta.mp4 b/tests/fixtures/content/video-with-meta.mp4
index 7cfc429f..5c5c4cb2 100644
Binary files a/tests/fixtures/content/video-with-meta.mp4 and b/tests/fixtures/content/video-with-meta.mp4 differ
diff --git a/tests/fixtures/gen-synthetic-content.mjs b/tests/fixtures/gen-synthetic-content.mjs
new file mode 100644
index 00000000..41814f78
--- /dev/null
+++ b/tests/fixtures/gen-synthetic-content.mjs
@@ -0,0 +1,348 @@
+#!/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 { writeFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { execSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { createRequire } from "node:module";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const CONTENT = join(__dirname, "content");
+const require = createRequire(join(__dirname, "../../apps/api/package.json"));
+const sharp = require("sharp");
+const QRCode = require("qrcode");
+
+// ── 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 ``;
+}
+
+// ── Minimal PDF generator ──────────────────────────────────
+
+function generatePdf(pageCount) {
+ 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 = `BT /F1 24 Tf 72 700 Td (Page ${i}) Tj ET`;
+ 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");
+}
+
+// ── SVG logo (project-owned, replaces ConvertICO brand) ───
+
+function projectSvg() {
+ return ``;
+}
+
+// ── 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 ``;
+}
+
+// ── 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;
+ }
+}
+
+// ── Main ───────────────────────────────────────────────────
+
+async function main() {
+ console.log("Regenerating Category A & B content fixtures...\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" },
+ });
+ writeFileSync(join(CONTENT, "qr-code.png"), qrPng);
+ console.log(" qr-code.png");
+
+ const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 });
+ writeFileSync(join(CONTENT, "qr-code.svg"), qrSvg);
+ console.log(" qr-code.svg");
+
+ const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer();
+ writeFileSync(join(CONTENT, "qr-code.avif"), qrAvif);
+ console.log(" 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();
+ writeFileSync(join(CONTENT, "barcode.png"), bcPng);
+ console.log(" barcode.png");
+
+ const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer();
+ writeFileSync(join(CONTENT, "barcode.avif"), bcAvif);
+ console.log(" barcode.avif");
+
+ // ── OCR text images (A) ──
+ console.log("OCR text images:");
+ const ocrEngSvg = ``;
+ const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer();
+ writeFileSync(join(CONTENT, "ocr-clean.png"), ocrEngPng);
+ console.log(" 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 = ``;
+ const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer();
+ writeFileSync(join(CONTENT, "ocr-japanese.png"), jpPng);
+ console.log(" ocr-japanese.png");
+
+ // ── PDFs (A) ──
+ console.log("PDFs:");
+ writeFileSync(join(CONTENT, "alt-2page.pdf"), generatePdf(2));
+ console.log(" alt-2page.pdf");
+ writeFileSync(join(CONTENT, "multipage-6.pdf"), generatePdf(6));
+ console.log(" multipage-6.pdf");
+
+ // ── SVG logo (B: replaces ConvertICO brand) ──
+ console.log("SVG logo (replacing ConvertICO brand):");
+ writeFileSync(join(CONTENT, "svg-logo.svg"), projectSvg());
+ console.log(" 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();
+ writeFileSync(join(CONTENT, "multi-face.webp"), mfWebp);
+ console.log(" multi-face.webp");
+
+ // ── Animated GIF (B: replaces copyrighted Simpsons clip) ──
+ console.log("Animated GIF (replacing copyrighted Simpsons clip):");
+ const gifOut = join(CONTENT, "animated-simpsons.gif");
+ ff(
+ `-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`,
+ "animated-simpsons.gif",
+ );
+
+ // ── Synthetic audio/video (A) ──
+ console.log("Synthetic audio/video:");
+
+ ff(
+ `-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 "${join(CONTENT, "audio-with-tags.mp3")}"`,
+ "audio-with-tags.mp3",
+ );
+
+ ff(
+ `-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` +
+ `-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` +
+ `-y "${join(CONTENT, "video-with-meta.mp4")}"`,
+ "video-with-meta.mp4",
+ );
+
+ ff(
+ `-f lavfi -i "testsrc2=duration=30:size=640x360:rate=24" ` +
+ `-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
+ `-c:v libx264 -pix_fmt yuv420p -preset ultrafast ` +
+ `-c:a aac -b:a 128k -ac 1 -movflags +faststart ` +
+ `-y "${join(CONTENT, "media-30s.mp4")}"`,
+ "media-30s.mp4",
+ );
+
+ ff(
+ `-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
+ `-ac 1 -c:a pcm_s16le ` +
+ `-y "${join(CONTENT, "media-30s.wav")}"`,
+ "media-30s.wav",
+ );
+
+ console.log("\nDone. Run gen-manifest.mjs to re-stamp hashes.");
+}
+
+main().catch((e) => {
+ console.error("FATAL:", e);
+ process.exit(1);
+});