import { test } from 'node:test'; import assert from 'node:assert/strict'; import sharp from 'sharp'; import { buildXmp, embedProvenance, readXmp, TRAINED_ALGORITHMIC_MEDIA, COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA, stripXmp, } from '../src/lib/provenance.ts'; const bild = (w = 240, h = 160) => sharp({ create: { width: w, height: h, channels: 3, background: '#c8463c' } }); test('Das XMP-Paket trägt den IPTC-Code und das Modell', () => { const xmp = buildXmp({ sourceType: TRAINED_ALGORITHMIC_MEDIA, model: 'google/gemini-3-pro-image' }); assert.match(xmp, /Iptc4xmpExt:DigitalSourceType="http:\/\/cv\.iptc\.org\/newscodes\/digitalsourcetype\/trainedAlgorithmicMedia"/); assert.match(xmp, /xmp:CreatorTool="Klarbild \(google\/gemini-3-pro-image\)"/); assert.match(xmp, / { const xmp = buildXmp({ sourceType: TRAINED_ALGORITHMIC_MEDIA, description: 'Lea & Phoenix "groß" — 3 < 5', }); assert.ok(!xmp.includes(''), 'unmaskiertes Tag im XMP'); assert.match(xmp, /Lea & Phoenix/); // Genau so viele öffnende wie schließende Klammern der bekannten Elemente. assert.equal((xmp.match(//g) || []).length, 1); }); for (const [name, machen] of [ ['PNG', async () => bild().png().toBuffer()], ['JPEG', async () => bild().jpeg().toBuffer()], ['JPEG mit dpi-Metadaten (wie im Auftrag)', async () => bild().withMetadata({ density: 300 }).jpeg({ quality: 92 }).toBuffer()], ] as const) { test(`${name}: Kennzeichnung wird geschrieben und das Bild bleibt heil`, async () => { const vorher = await machen(); const nachher = embedProvenance(vorher, { sourceType: TRAINED_ALGORITHMIC_MEDIA, model: 'test/modell', description: 'Prüflauf', }); assert.ok(nachher.length > vorher.length, 'nichts eingebettet'); // 1) Kennzeichnung ist wieder auslesbar … const xmp = readXmp(nachher); assert.ok(xmp, 'XMP nicht gefunden'); assert.match(xmp!, /trainedAlgorithmicMedia/); // 2) … und sharp liest die Datei weiterhin, mit unveränderten Maßen. const m = await sharp(nachher).metadata(); const m0 = await sharp(vorher).metadata(); assert.equal(m.width, m0.width); assert.equal(m.height, m0.height); assert.equal(m.format, m0.format); if (m0.density) assert.equal(m.density, m0.density, 'dpi verloren'); // 3) Die Bildpunkte selbst sind unangetastet. const a = await sharp(vorher).raw().toBuffer(); const b = await sharp(nachher).raw().toBuffer(); assert.ok(a.equals(b), 'Bildinhalt hat sich verändert'); }); } test('Bereinigte Fotos bekommen den Code für verändertes Material', () => { const xmp = buildXmp({ sourceType: COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA }); assert.match(xmp, /compositeWithTrainedAlgorithmicMedia/); }); test('Unbekannte Formate und Schrott kommen unverändert zurück', () => { for (const b of [Buffer.from('kein bild'), Buffer.alloc(0), Buffer.from([0x52, 0x49, 0x46, 0x46])]) { assert.ok(embedProvenance(b, { sourceType: TRAINED_ALGORITHMIC_MEDIA }).equals(b)); } }); test('Zweimal kennzeichnen bricht die Datei nicht', async () => { const png = await bild().png().toBuffer(); const einmal = embedProvenance(png, { sourceType: TRAINED_ALGORITHMIC_MEDIA }); const zweimal = embedProvenance(einmal, { sourceType: TRAINED_ALGORITHMIC_MEDIA }); const m = await sharp(zweimal).metadata(); assert.equal(m.width, 240); }); test('Die APP1-Kennung endet auf ein echtes Nullbyte (sonst erkennt es kein Werkzeug)', async () => { const jpg = embedProvenance(await bild().jpeg().toBuffer(), { sourceType: TRAINED_ALGORITHMIC_MEDIA }); const i = jpg.indexOf('http://ns.adobe.com/xap/1.0/'); assert.ok(i > 0, 'Kennung nicht gefunden'); assert.equal(jpg[i + 28], 0x00, 'Kennung endet nicht auf 0x00'); }); test('Zweimal kennzeichnen ersetzt die alte Angabe, statt eine zweite anzuhängen', async () => { for (const [name, machen] of [ ['PNG', async () => bild().png().toBuffer()], ['JPEG', async () => bild().jpeg().toBuffer()], ] as const) { const roh = await machen(); const alt = embedProvenance(roh, { sourceType: TRAINED_ALGORITHMIC_MEDIA, model: 'altes/modell' }); const neu = embedProvenance(alt, { sourceType: TRAINED_ALGORITHMIC_MEDIA, model: 'neues/modell' }); // Genau **ein** Paket, und zwar das neue. const pakete = neu.toString('latin1').split(' { const png = await bild().png().toBuffer(); assert.ok(stripXmp(png).equals(png)); const jpg = await bild().jpeg().toBuffer(); assert.ok(stripXmp(jpg).equals(jpg)); });