test: reorganize fixture files into modality-first layout (phase 6b)

Move all fixture files from flat/mixed dirs (content/, media/, documents/,
formats/, hostile/, root loose) into the modality-first hierarchy:
image/{valid,formats,edge,hostile}, video/{valid,formats,hostile},
audio/{valid,formats,hostile}, document/{valid,formats,edge,hostile},
data/valid/, security/. Update index.ts paths, fixtureDir aliases,
all literal refs in 17 e2e/qa/script files, manifest.json, and the
three generator scripts. 163 files moved, 0 dropped, 100 new tests
from expanded document scan.
This commit is contained in:
SnapOtter
2026-06-20 05:51:17 +08:00
parent 1727a7a73e
commit 941ed27912
199 changed files with 560 additions and 429 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
// make-tiny-pptx.mjs
//
// Builds a minimal ECMA-376 (OOXML) presentation skeleton and writes it to
// tests/fixtures/documents/tiny.pptx using adm-zip (root devDep).
// tests/fixtures/document/formats/tiny.pptx using adm-zip (root devDep).
//
// ACCEPTANCE (2026-06-12, snapotter/snapotter:latest + libreoffice-impress):
// soffice --headless --norestore --nolockcheck --nodefault \
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
import AdmZip from "adm-zip";
const __dirname = dirname(fileURLToPath(import.meta.url));
const outPath = join(__dirname, "..", "..", "tests", "fixtures", "documents", "tiny.pptx");
const outPath = join(__dirname, "..", "..", "tests", "fixtures", "document", "formats", "tiny.pptx");
const zip = new AdmZip();
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Generates the hostile-input fixtures in tests/fixtures/hostile/.
* Generates the hostile-input fixtures in tests/fixtures/image/hostile/.
*
* Deterministic on purpose: re-running produces byte-identical files so the
* committed fixtures never churn. No dependencies (the bomb PNG is built by
@@ -14,11 +14,11 @@ import { fileURLToPath } from "node:url";
import zlib from "node:zlib";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const outDir = join(root, "tests", "fixtures", "hostile");
const outDir = join(root, "tests", "fixtures", "image", "hostile");
mkdirSync(outDir, { recursive: true });
// --- 1. truncated.jpg: a real JPEG cut off at 40% ---------------------------
const realJpeg = readFileSync(join(root, "tests", "fixtures", "sample-photo.jpg"));
const realJpeg = readFileSync(join(root, "tests", "fixtures", "image", "valid", "sample-photo.jpg"));
writeFileSync(
join(outDir, "truncated.jpg"),
realJpeg.subarray(0, Math.floor(realJpeg.length * 0.4)),
@@ -38,7 +38,7 @@ for (let i = 0; i < garbage.length; i++) {
writeFileSync(join(outDir, "garbage.jpg"), garbage);
// --- 4. png-bytes.jpg: valid PNG content with a lying .jpg extension --------
const realPng = readFileSync(join(root, "tests", "fixtures", "test-200x150.png"));
const realPng = readFileSync(join(root, "tests", "fixtures", "image", "valid", "test-200x150.png"));
writeFileSync(join(outDir, "png-bytes.jpg"), realPng);
// --- 5. bomb-50000x50000.png: tiny file whose header claims 2,500 megapixels
+11 -9
View File
@@ -13,9 +13,11 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, "..");
const mediaDir = join(root, "tests/fixtures/media");
const docsDir = join(root, "tests/fixtures/documents");
mkdirSync(mediaDir, { recursive: true });
const videoDir = join(root, "tests/fixtures/video/formats");
const audioDir = join(root, "tests/fixtures/audio/formats");
const docsDir = join(root, "tests/fixtures/document/formats");
mkdirSync(videoDir, { recursive: true });
mkdirSync(audioDir, { recursive: true });
mkdirSync(docsDir, { recursive: true });
const ffmpeg = process.env.FFMPEG_PATH || "ffmpeg";
@@ -58,7 +60,7 @@ run([
"-b:a",
"32k",
"-shortest",
join(mediaDir, "tiny.mp4"),
join(videoDir, "tiny.mp4"),
]);
// 1s sine mp3
run([
@@ -70,7 +72,7 @@ run([
"libmp3lame",
"-b:a",
"32k",
join(mediaDir, "tiny.mp3"),
join(audioDir, "tiny.mp3"),
]);
// 1s sine wav
run([
@@ -82,7 +84,7 @@ run([
"pcm_s16le",
"-ar",
"8000",
join(mediaDir, "tiny.wav"),
join(audioDir, "tiny.wav"),
]);
// Minimal OOXML/EPUB containers via archiver (resolved from apps/api).
@@ -154,8 +156,8 @@ await writeZip(
// Encrypted PDF fixture derived from test-3page.pdf via qpdf (AES-256).
const qpdf = whichBin("QPDF_PATH", "qpdf");
if (qpdf) {
const srcPdf = join(root, "tests/fixtures/test-3page.pdf");
const encPdf = join(docsDir, "encrypted.pdf");
const srcPdf = join(root, "tests/fixtures/document/valid/test-3page.pdf");
const encPdf = join(root, "tests/fixtures/document/valid/encrypted.pdf");
const qres = spawnSync(qpdf, [srcPdf, "--encrypt", "test123", "owner123", "256", "--", encPdf], {
stdio: "inherit",
});
@@ -192,4 +194,4 @@ A paragraph with **bold** and a list:
`,
);
console.log("Fixtures written to tests/fixtures/media and tests/fixtures/documents");
console.log("Fixtures written to tests/fixtures/{video,audio}/formats and tests/fixtures/document/formats");
+1 -1
View File
@@ -318,7 +318,7 @@ test.describe("Editor Menu Bar", () => {
const fileChooserPromise = page.waitForEvent("filechooser");
await page.click('[data-testid="menu-item-open"]');
const chooser = await fileChooserPromise;
await chooser.setFiles("tests/fixtures/test-200x150.png");
await chooser.setFiles("tests/fixtures/image/valid/test-200x150.png");
await page.waitForTimeout(1000);
await page.click('[data-testid="menu-select"]');
await page.click('[data-testid="menu-item-all"]');
+1 -1
View File
@@ -55,7 +55,7 @@ export async function drawOnCanvas(
export async function loadTestImage(page: Page): Promise<void> {
await page.route("**/test-fixture.png", (route) =>
route.fulfill({
path: "tests/fixtures/test-200x150.png",
path: "tests/fixtures/image/valid/test-200x150.png",
contentType: "image/png",
}),
);
+8 -1
View File
@@ -1,7 +1,14 @@
import path from "node:path";
import { expect, test, waitForProcessing } from "./helpers";
const PDF_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "test-3page.pdf");
const PDF_FIXTURE = path.join(
process.cwd(),
"tests",
"fixtures",
"document",
"valid",
"test-3page.pdf",
);
test.describe("Document display mode (rotate-pdf)", () => {
test("uploads a PDF, rotates it, and shows the document canvas with processed result", async ({
+5 -4
View File
@@ -2,8 +2,9 @@ import path from "node:path";
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
const fixtureFormat = (name: string) =>
path.join(process.cwd(), "tests", "fixtures", "formats", name);
const fixtureRoot = (name: string) => path.join(process.cwd(), "tests", "fixtures", name);
path.join(process.cwd(), "tests", "fixtures", "image", "formats", name);
const fixtureImage = (name: string) =>
path.join(process.cwd(), "tests", "fixtures", "image", "valid", name);
async function uploadFixture(page: import("@playwright/test").Page, filePath: string) {
const fileChooserPromise = page.waitForEvent("filechooser");
@@ -39,7 +40,7 @@ test.describe("Format upload and resize processing", () => {
}
test("HEIC uploads and resizes", async ({ loggedInPage: page }) => {
await page.goto("/resize");
await uploadFixture(page, fixtureRoot("test-200x150.heic"));
await uploadFixture(page, fixtureImage("test-200x150.heic"));
await page.locator("input[placeholder='Auto']").first().fill("25");
await page.getByRole("button", { name: "Resize" }).click();
await waitForProcessing(page);
@@ -108,7 +109,7 @@ test.describe("HEIC-to-JPG conversion", () => {
test.describe.configure({ timeout: 60_000 });
test("uploads HEIC, converts to JPG", async ({ loggedInPage: page }) => {
await page.goto("/convert");
await uploadFixture(page, fixtureRoot("test-200x150.heic"));
await uploadFixture(page, fixtureImage("test-200x150.heic"));
await expect(page.getByText(/heic/i).first()).toBeVisible({ timeout: 10_000 });
await page.selectOption("#convert-target-format", "jpg");
await page.getByRole("button", { name: /convert/i }).click();
+8 -1
View File
@@ -256,7 +256,14 @@ test.describe("GUI Essential Tools", () => {
}) => {
await page.goto("/crop");
const portraitPath = path.join(process.cwd(), "tests", "fixtures", "test-portrait-tall.png");
const portraitPath = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"edge",
"test-portrait-tall.png",
);
const fileChooserPromise = page.waitForEvent("filechooser");
await page.locator("[class*='border-dashed']").first().click();
const fileChooser = await fileChooserPromise;
+32 -4
View File
@@ -197,7 +197,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
await page.goto("/compare");
// Set second image via file input
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
const testFixture = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-200x150.png",
);
await page.locator("#compare-second-image").setInputFiles(testFixture);
await page.waitForTimeout(300);
@@ -216,7 +223,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
await page.goto("/compare");
await uploadTestImage(page);
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
const testFixture = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-200x150.png",
);
await page.locator("#compare-second-image").setInputFiles(testFixture);
await page.waitForTimeout(300);
@@ -227,7 +241,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
await page.goto("/compare");
await uploadTestImage(page);
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
const testFixture = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-200x150.png",
);
await page.locator("#compare-second-image").setInputFiles(testFixture);
await page.waitForTimeout(300);
@@ -241,7 +262,14 @@ test.describe("GUI Expanded Tool Coverage", () => {
await page.goto("/compare");
await uploadTestImage(page);
const testFixture = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
const testFixture = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-200x150.png",
);
await page.locator("#compare-second-image").setInputFiles(testFixture);
await page.waitForTimeout(300);
+9 -3
View File
@@ -86,7 +86,9 @@ test.describe("GUI Format & Conversion Tools", () => {
const fileChooserPromise = page.waitForEvent("filechooser");
await page.locator("[class*='border-dashed']").first().click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "test-100x100.svg"));
await fileChooser.setFiles(
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "test-100x100.svg"),
);
await page.waitForTimeout(500);
await page.getByRole("button", { name: "Scale Factor" }).click();
@@ -294,7 +296,9 @@ test.describe("GUI Format & Conversion Tools", () => {
const fileChooserPromise = page.waitForEvent("filechooser");
await page.locator("[class*='border-dashed']").first().click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "animated.gif"));
await fileChooser.setFiles(
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "animated.gif"),
);
await page.waitForTimeout(500);
await page.getByRole("button", { name: "Speed" }).first().click();
@@ -308,7 +312,9 @@ test.describe("GUI Format & Conversion Tools", () => {
const fileChooserPromise = page.waitForEvent("filechooser");
await page.locator("[class*='border-dashed']").first().click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(process.cwd(), "tests", "fixtures", "animated.gif"));
await fileChooser.setFiles(
path.join(process.cwd(), "tests", "fixtures", "image", "valid", "animated.gif"),
);
await page.waitForTimeout(500);
await page.getByRole("button", { name: "Extract" }).first().click();
+8 -1
View File
@@ -425,7 +425,14 @@ test.describe("GUI Watermark & Overlay Tools", () => {
await page.goto("/compose");
await uploadBaseImage(page);
const webpPath = path.join(process.cwd(), "tests", "fixtures", "test-50x50.webp");
const webpPath = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-50x50.webp",
);
await uploadOverlayImage(page, webpPath);
await page.getByTestId("compose-submit").click();
+8 -1
View File
@@ -1,7 +1,14 @@
import path from "node:path";
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
const FIXTURE_PNG = path.join(process.cwd(), "tests", "fixtures", "test-200x150.png");
const FIXTURE_PNG = path.join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-200x150.png",
);
// ---------------------------------------------------------------------------
// GUI E2E: Utility Tools
+1 -1
View File
@@ -103,7 +103,7 @@ export function getTestImagePath(): string {
// getTestHeicPath() — return a small HEIC test image (from fixtures)
// ---------------------------------------------------------------------------
export function getTestHeicPath(): string {
return path.join(process.cwd(), "tests", "fixtures", "test-200x150.heic");
return path.join(process.cwd(), "tests", "fixtures", "image", "valid", "test-200x150.heic");
}
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -1,7 +1,7 @@
import path from "node:path";
import { expect, test, waitForProcessing } from "./helpers";
const MP4_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "media", "tiny.mp4");
const MP4_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "video", "formats", "tiny.mp4");
test.describe("Media-player display mode (mute-video)", () => {
test("uploads a video, mutes it, and shows the media player with processed result", async ({
+3 -3
View File
@@ -81,7 +81,7 @@ test.describe("OCR / Text Extraction", () => {
test("uploads image and OCR processing completes", async ({ loggedInPage: page }) => {
await skipIfFeatureNotInstalled(page);
await uploadOcrFile(page, "tests/fixtures/test-portrait.jpg");
await uploadOcrFile(page, "tests/fixtures/image/valid/test-portrait.jpg");
await submitOcr(page);
const hasText = await page.getByTestId("ocr-result-text").isVisible();
@@ -92,7 +92,7 @@ test.describe("OCR / Text Extraction", () => {
test("copy button is visible after OCR completes", async ({ loggedInPage: page }) => {
await skipIfFeatureNotInstalled(page);
await uploadOcrFile(page, "tests/fixtures/test-portrait.jpg");
await uploadOcrFile(page, "tests/fixtures/image/valid/test-portrait.jpg");
await submitOcr(page);
await expect(page.getByText("Copy")).toBeVisible();
@@ -101,7 +101,7 @@ test.describe("OCR / Text Extraction", () => {
test("shows 'no text detected' for blank image", async ({ loggedInPage: page }) => {
await skipIfFeatureNotInstalled(page);
await uploadOcrFile(page, "tests/fixtures/test-blank.png");
await uploadOcrFile(page, "tests/fixtures/image/edge/test-blank.png");
await submitOcr(page);
await expect(page.getByText("No text detected")).toBeVisible();
+8 -1
View File
@@ -1,7 +1,14 @@
import path from "node:path";
import { expect, test, waitForProcessing } from "./helpers";
const PDF_FIXTURE = path.join(process.cwd(), "tests", "fixtures", "test-3page.pdf");
const PDF_FIXTURE = path.join(
process.cwd(),
"tests",
"fixtures",
"document",
"valid",
"test-3page.pdf",
);
test.describe("PDF to Image tool", () => {
test("shows page thumbnails after uploading a PDF", async ({ loggedInPage: page }) => {
+8 -1
View File
@@ -133,7 +133,14 @@ test.describe("State bleed between tools", () => {
await addMoreBtn.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(
require("node:path").join(process.cwd(), "tests", "fixtures", "test-50x50.webp"),
require("node:path").join(
process.cwd(),
"tests",
"fixtures",
"image",
"valid",
"test-50x50.webp",
),
);
await page.waitForTimeout(500);
}
-1
View File
@@ -1 +0,0 @@
Phase 6 reorg moves audio fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
-1
View File
@@ -1 +0,0 @@
Phase 6 reorg moves data fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
-1
View File
@@ -1 +0,0 @@
Phase 6 reorg moves document fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.
+21 -10
View File
@@ -1,22 +1,33 @@
#!/usr/bin/env node
// Stamps sha256 + bytes for the real `content/` heroes into manifest.json.
// Provenance (sourceUrl/license) is filled by hand in Step 2 -- this only fills hashes.
// Stamps sha256 + bytes for real fixture assets into manifest.json.
// Provenance (sourceUrl/license) is filled by hand -- this only fills hashes.
import { createHash } from "node:crypto";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = dirname(fileURLToPath(import.meta.url));
const contentDir = join(ROOT, "content");
const existing = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf8"));
const byPath = new Map(existing.assets.map((a) => [a.path, a]));
for (const name of readdirSync(contentDir)) {
const rel = `content/${name}`;
const bytes = readFileSync(join(contentDir, name));
const sha256 = createHash("sha256").update(bytes).digest("hex");
const prev = byPath.get(rel) ?? { path: rel, sourceUrl: "UNVERIFIED", license: "UNVERIFIED", toolsServed: [] };
byPath.set(rel, { ...prev, sha256, bytes: bytes.length });
// Scan each modality valid dir (replaces the old content/ scan)
const SCAN_DIRS = [
"image/valid",
"video/valid",
"audio/valid",
"document/valid",
];
for (const dir of SCAN_DIRS) {
const absDir = join(ROOT, dir);
if (!existsSync(absDir)) continue;
for (const name of readdirSync(absDir)) {
const rel = `${dir}/${name}`;
const bytes = readFileSync(join(absDir, name));
const sha256 = createHash("sha256").update(bytes).digest("hex");
const prev = byPath.get(rel) ?? { path: rel, sourceUrl: "UNVERIFIED", license: "UNVERIFIED", toolsServed: [] };
byPath.set(rel, { ...prev, sha256, bytes: bytes.length });
}
}
const assets = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
writeFileSync(join(ROOT, "manifest.json"), JSON.stringify({ assets }, null, 2));
+20 -17
View File
@@ -21,7 +21,10 @@ import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CONTENT = join(__dirname, "content");
const IMAGE_VALID = join(__dirname, "image/valid");
const VIDEO_VALID = join(__dirname, "video/valid");
const AUDIO_VALID = join(__dirname, "audio/valid");
const DOC_VALID = join(__dirname, "document/valid");
const require = createRequire(join(__dirname, "../../apps/api/package.json"));
const sharp = require("sharp");
const QRCode = require("qrcode");
@@ -223,15 +226,15 @@ async function main() {
const qrPng = await QRCode.toBuffer(qrData, {
width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" },
});
writeFileSync(join(CONTENT, "qr-code.png"), qrPng);
writeFileSync(join(IMAGE_VALID, "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);
writeFileSync(join(IMAGE_VALID, "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);
writeFileSync(join(IMAGE_VALID, "qr-code.avif"), qrAvif);
console.log(" qr-code.avif");
// ── Barcodes (A) ──
@@ -239,11 +242,11 @@ async function main() {
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);
writeFileSync(join(IMAGE_VALID, "barcode.png"), bcPng);
console.log(" barcode.png");
const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer();
writeFileSync(join(CONTENT, "barcode.avif"), bcAvif);
writeFileSync(join(IMAGE_VALID, "barcode.avif"), bcAvif);
console.log(" barcode.avif");
// ── OCR text images (A) ──
@@ -253,7 +256,7 @@ async function main() {
<text x="400" y="115" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="black">The quick brown fox 12345</text>
</svg>`;
const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer();
writeFileSync(join(CONTENT, "ocr-clean.png"), ocrEngPng);
writeFileSync(join(IMAGE_VALID, "ocr-clean.png"), ocrEngPng);
console.log(" ocr-clean.png");
const jpText =
@@ -273,31 +276,31 @@ async function main() {
${lines.map((line, i) => `<text x="16" y="${30 + i * 28}" font-family="Hiragino Sans, Hiragino Kaku Gothic Pro, sans-serif" font-size="20" fill="black">${line}</text>`).join("\n ")}
</svg>`;
const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer();
writeFileSync(join(CONTENT, "ocr-japanese.png"), jpPng);
writeFileSync(join(IMAGE_VALID, "ocr-japanese.png"), jpPng);
console.log(" ocr-japanese.png");
// ── PDFs (A) ──
console.log("PDFs:");
writeFileSync(join(CONTENT, "alt-2page.pdf"), generatePdf(2));
writeFileSync(join(DOC_VALID, "alt-2page.pdf"), generatePdf(2));
console.log(" alt-2page.pdf");
writeFileSync(join(CONTENT, "multipage-6.pdf"), generatePdf(6));
writeFileSync(join(DOC_VALID, "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());
writeFileSync(join(IMAGE_VALID, "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);
writeFileSync(join(IMAGE_VALID, "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");
const gifOut = join(IMAGE_VALID, "animated-simpsons.gif");
ff(
`-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`,
"animated-simpsons.gif",
@@ -312,14 +315,14 @@ async function main() {
`-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")}"`,
`-y "${join(AUDIO_VALID, "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")}"`,
`-y "${join(VIDEO_VALID, "video-with-meta.mp4")}"`,
"video-with-meta.mp4",
);
@@ -328,14 +331,14 @@ async function main() {
`-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")}"`,
`-y "${join(VIDEO_VALID, "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")}"`,
`-y "${join(AUDIO_VALID, "media-30s.wav")}"`,
"media-30s.wav",
);
-1
View File
@@ -1 +0,0 @@
Phase 6 reorg moves image fixtures here (valid/ formats/ edge/ hostile/). See ../README.md.

Before

Width:  |  Height:  |  Size: 91 B

After

Width:  |  Height:  |  Size: 91 B

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 287 B

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 511 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 799 KiB

After

Width:  |  Height:  |  Size: 799 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 190 KiB

Some files were not shown because too many files have changed in this diff Show More