import { unzipSync } from "fflate"; import { describe, expect, it } from "vitest"; import { createBatchZip } from "../../src/image/batch-zip"; import { parseRecipe, serializeRecipe } from "../../src/image/recipes"; import { DEFAULT_OUTPUT, DEFAULT_RECIPE, type ProcessImageResult, } from "../../src/image/types"; describe("versioned image recipes", () => { it("round-trips bounded transform and output settings", () => { const recipe = { ...DEFAULT_RECIPE, crop: { x: 0.1, y: 0.2, width: 0.5, height: 0.6 }, rotation: 90 as const, }; expect(parseRecipe(serializeRecipe(recipe, DEFAULT_OUTPUT))).toMatchObject({ schema: "de.add-ideas.image-tools.recipe.v1", recipe, output: DEFAULT_OUTPUT, policy: { metadataCopied: false, colorProfilePreserved: false }, }); }); it("rejects out-of-image crops and substituted formats", () => { const value = JSON.parse( serializeRecipe(DEFAULT_RECIPE, DEFAULT_OUTPUT), ) as Record; (value.recipe as { crop: { x: number } }).crop.x = 1.5; expect(() => parseRecipe(JSON.stringify(value))).toThrow( /inside the image/iu, ); (value.recipe as { crop: { x: number } }).crop.x = 0; (value.output as { format: string }).format = "avif"; expect(() => parseRecipe(JSON.stringify(value))).toThrow(/unsupported/iu); }); }); describe("bounded batch ZIP", () => { it("creates one deterministic-named archive with reports and a manifest", async () => { const first = result("same-edited.png", new Uint8Array([1, 2, 3])); const second = result("same-edited.png", new Uint8Array([4, 5])); const blob = await createBatchZip([ { sourceName: "first.png", result: first }, { sourceName: "second.png", result: second }, ]); const entries = unzipSync(new Uint8Array(await blob.arrayBuffer())); expect(Object.keys(entries).sort()).toEqual([ "batch-manifest.json", "same-edited-2.png", "same-edited-2.report.json", "same-edited.png", "same-edited.report.json", ]); expect( JSON.parse(new TextDecoder().decode(entries["batch-manifest.json"])), ).toMatchObject({ schema: "de.add-ideas.image-tools.batch.v1", items: [ { sourceName: "first.png", outputName: "same-edited.png" }, { sourceName: "second.png", outputName: "same-edited-2.png" }, ], }); }); }); function result(name: string, bytes: Uint8Array): ProcessImageResult { const owned = new Uint8Array(bytes.byteLength); owned.set(bytes); const blob = new Blob([owned.buffer], { type: "image/png" }); return { blob, width: 1, height: 1, renderedWidth: 1, renderedHeight: 1, report: { schemaVersion: 1, source: { name: "source.png", bytes: bytes.byteLength, format: "png", width: 1, height: 1, orientation: 1, metadata: { exif: false, xmp: false, icc: false, comments: false, text: false, physicalDimensions: false, multiPicture: false, }, }, transform: DEFAULT_RECIPE, output: { name, bytes: bytes.byteLength, mimeType: "image/png", width: 1, height: 1, quality: null, background: null, }, metadataPolicy: { copied: false, statement: "not copied" }, colorPolicy: { profilePreserved: false, statement: "not preserved" }, limitations: [], warnings: [], }, }; }