import { crc32 } from "@add-ideas/toolbox-helpers"; import { zlibSync } from "fflate"; const encoder = new TextEncoder(); export function jpegFixture(options: { trailing?: boolean } = {}): Uint8Array { const xmp = encoder.encode( "http://ns.adobe.com/xap/1.0/\0" + 'Alice Example48.1 NFixture Cameradoc-123', ); const jfif = concat( encoder.encode("JFIF\0"), bytes(1, 2, 0), u16be(72), u16be(72), bytes(0, 0), ); const iptc = concat( iptcDataset(80, "Alice Reporter"), iptcDataset(90, "Berlin"), iptcDataset(120, "Private caption"), ); const photoshop = concat( encoder.encode("Photoshop 3.0\0"), encoder.encode("8BIM"), u16be(0x0404), bytes(0, 0), u32be(iptc.byteLength), iptc, iptc.byteLength % 2 ? bytes(0) : bytes(), ); const body = concat( bytes(0xff, 0xd8), jpegSegment(0xe0, jfif), jpegSegment(0xe1, concat(encoder.encode("Exif\0\0"), tiffFixture())), jpegSegment(0xe1, xmp), jpegSegment( 0xe2, concat( encoder.encode("ICC_PROFILE\0"), bytes(1, 1), encoder.encode("icc"), ), ), jpegSegment(0xeb, encoder.encode("jumb/c2pa test manifest")), jpegSegment(0xed, photoshop), jpegSegment(0xfe, encoder.encode("private jpeg comment")), jpegSegment(0xc0, bytes(8, 0, 2, 0, 3, 1, 1, 0x11, 0)), bytes(0xff, 0xda, 0xff, 0xd9), ); return options.trailing ? concat(body, encoder.encode("hidden")) : body; } export function pngFixture( options: { cycleTiff?: boolean; inflatedCommentBytes?: number; animated?: boolean; trailing?: boolean; } = {}, ): Uint8Array { const width = 2; const height = 1; const ihdr = concat(u32be(width), u32be(height), bytes(8, 6, 0, 0, 0)); const xmp = encoder.encode( "XML:com.adobe.xmp\0\0\0\0\0" + 'PNG AlicePNG Fixture', ); const inflated = encoder.encode( "x".repeat(options.inflatedCommentBytes ?? 32), ); const scanline = bytes(0, 255, 0, 0, 255, 0, 0, 255, 255); const chunks = [ pngChunk("IHDR", ihdr), pngChunk("tEXt", encoder.encode("Author\0Alice PNG")), pngChunk( "zTXt", concat(encoder.encode("Comment\0"), bytes(0), zlibSync(inflated)), ), pngChunk("iTXt", xmp), pngChunk("eXIf", tiffFixture({ cycle: options.cycleTiff })), pngChunk( "iCCP", concat( encoder.encode("fixture profile\0"), bytes(0), zlibSync(encoder.encode("ICC profile bytes")), ), ), pngChunk("vpAg", encoder.encode("private ancillary data")), ...(options.animated ? [pngChunk("acTL", concat(u32be(2), u32be(0)))] : []), pngChunk("IDAT", zlibSync(scanline)), pngChunk("IEND", bytes()), ]; const image = concat( bytes(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a), ...chunks, ); return options.trailing ? concat(image, encoder.encode("trailing secret")) : image; } export function webpFixture(options: { animated?: boolean } = {}): Uint8Array { const vp8x = bytes(options.animated ? 0x02 : 0, 0, 0, 0, 3, 0, 0, 2, 0, 0); const dimensions = 3 | (2 << 14); const vp8l = concat(bytes(0x2f), u32le(dimensions)); const xmp = encoder.encode( 'WebP Alice13.4 E', ); const chunks = concat( riffChunk("VP8X", vp8x), riffChunk("VP8L", vp8l), riffChunk("EXIF", concat(encoder.encode("Exif\0\0"), tiffFixture())), riffChunk("XMP ", xmp), riffChunk("ICCP", encoder.encode("ICC profile bytes")), ...(options.animated ? [riffChunk("ANIM", bytes(0, 0, 0, 0, 0, 0))] : []), ); return concat( encoder.encode("RIFF"), u32le(chunks.byteLength + 4), encoder.encode("WEBP"), chunks, ); } export function tiffFixture( options: { cycle?: boolean; orientation?: number } = {}, ): Uint8Array { const make = encoder.encode("CameraCo\0"); const date = encoder.encode("2026:09:01 12:34:56\0"); const gpsDate = encoder.encode("2026:09:01\0"); const mainOffset = 8; const mainEntries = 4; const mainEnd = mainOffset + 2 + mainEntries * 12; const dataStart = mainEnd + 4; const makeOffset = dataStart; const dateOffset = makeOffset + make.byteLength; const gpsOffset = dateOffset + date.byteLength; const gpsEntries = 2; const gpsEnd = gpsOffset + 2 + gpsEntries * 12; const gpsDateOffset = gpsEnd + 4; const result = new Uint8Array(gpsDateOffset + gpsDate.byteLength); const view = new DataView(result.buffer); result.set(bytes(0x49, 0x49), 0); view.setUint16(2, 42, true); view.setUint32(4, mainOffset, true); view.setUint16(mainOffset, mainEntries, true); writeIfdEntry(view, mainOffset + 2, 0x010f, 2, make.byteLength, makeOffset); writeIfdEntry(view, mainOffset + 14, 0x0112, 3, 1, options.orientation ?? 1); writeIfdEntry(view, mainOffset + 26, 0x0132, 2, date.byteLength, dateOffset); writeIfdEntry(view, mainOffset + 38, 0x8825, 4, 1, gpsOffset); view.setUint32(mainEnd, options.cycle ? mainOffset : 0, true); result.set(make, makeOffset); result.set(date, dateOffset); view.setUint16(gpsOffset, gpsEntries, true); writeIfdEntry(view, gpsOffset + 2, 1, 2, 2, 0x4e); writeIfdEntry(view, gpsOffset + 14, 29, 2, gpsDate.byteLength, gpsDateOffset); view.setUint32(gpsEnd, 0, true); result.set(gpsDate, gpsDateOffset); return result; } export function malformedPngLengthFixture(): Uint8Array { const image = pngFixture(); const malformed = image.slice(); new DataView(malformed.buffer).setUint32(8, 0xfffffff0, false); return malformed; } export function toArrayBuffer(value: Uint8Array): ArrayBuffer { return value.slice().buffer as ArrayBuffer; } function jpegSegment(marker: number, payload: Uint8Array): Uint8Array { return concat(bytes(0xff, marker), u16be(payload.byteLength + 2), payload); } function iptcDataset(dataset: number, value: string): Uint8Array { const encoded = encoder.encode(value); return concat(bytes(0x1c, 2, dataset), u16be(encoded.byteLength), encoded); } function pngChunk(type: string, data: Uint8Array): Uint8Array { const typeBytes = encoder.encode(type); const checksumInput = concat(typeBytes, data); return concat( u32be(data.byteLength), checksumInput, u32be(crc32(checksumInput, 16 * 1024 * 1024)), ); } function riffChunk(type: string, data: Uint8Array): Uint8Array { return concat( encoder.encode(type), u32le(data.byteLength), data, data.byteLength % 2 ? bytes(0) : bytes(), ); } function writeIfdEntry( view: DataView, offset: number, tag: number, type: number, count: number, value: number, ): void { view.setUint16(offset, tag, true); view.setUint16(offset + 2, type, true); view.setUint32(offset + 4, count, true); if (type === 3 && count === 1) view.setUint16(offset + 8, value, true); else view.setUint32(offset + 8, value, true); } function concat(...parts: readonly Uint8Array[]): Uint8Array { const size = parts.reduce((total, part) => total + part.byteLength, 0); const result = new Uint8Array(size); let offset = 0; for (const part of parts) { result.set(part, offset); offset += part.byteLength; } return result; } function bytes(...values: readonly number[]): Uint8Array { return Uint8Array.from(values); } function u16be(value: number): Uint8Array { return bytes((value >>> 8) & 0xff, value & 0xff); } function u32be(value: number): Uint8Array { return bytes( (value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff, ); } function u32le(value: number): Uint8Array { return bytes( value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, (value >>> 24) & 0xff, ); }