@sweberdev/witness 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  createDisclosure: () => createDisclosure,
27
27
  createMarking: () => createMarking,
28
28
  detectImageFormat: () => detectImageFormat,
29
+ detectMediaFormat: () => detectMediaFormat,
29
30
  disclosureText: () => disclosureText,
30
31
  escapeHtml: () => escapeHtml,
31
32
  format: () => format,
@@ -36,7 +37,9 @@ __export(index_exports, {
36
37
  isAiSourceType: () => isAiSourceType,
37
38
  labelHtml: () => labelHtml,
38
39
  labelText: () => labelText,
40
+ markFile: () => markFile,
39
41
  markImage: () => markImage,
42
+ markMedia: () => markMedia,
40
43
  markingAttributes: () => markingAttributes,
41
44
  markingJsonLd: () => markingJsonLd,
42
45
  markingMetaTags: () => markingMetaTags,
@@ -44,6 +47,8 @@ __export(index_exports, {
44
47
  nextMetadata: () => nextMetadata,
45
48
  parseSourceType: () => parseSourceType,
46
49
  readImageMarking: () => readImageMarking,
50
+ readMarking: () => readMarking,
51
+ readMediaMarking: () => readMediaMarking,
47
52
  readTextWatermark: () => readTextWatermark,
48
53
  registerLocale: () => registerLocale,
49
54
  registeredLocales: () => registeredLocales,
@@ -1182,6 +1187,293 @@ function unescapeXml(value) {
1182
1187
  return value.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1183
1188
  }
1184
1189
 
1190
+ // src/media.ts
1191
+ var XMP_UUID = hex("be7acfcb97a942e89c71999491e3afac");
1192
+ var C2PA_UUID = hex("d8fec3d61b0e483c92975828877ec481");
1193
+ function detectMediaFormat(bytes) {
1194
+ if (bytes.length >= 10 && ascii2(bytes, 0, 3) === "ID3") return "mp3";
1195
+ if (isMpegAudioFrame(bytes, 0)) return "mp3";
1196
+ if (bytes.length >= 12 && ascii2(bytes, 0, 4) === "RIFF" && ascii2(bytes, 8, 4) === "WAVE") {
1197
+ return "wav";
1198
+ }
1199
+ if (bytes.length >= 12 && ascii2(bytes, 4, 4) === "ftyp") return "mp4";
1200
+ return null;
1201
+ }
1202
+ function markMedia(bytes, input = {}, options = {}) {
1203
+ const format2 = detectMediaFormat(bytes);
1204
+ if (!format2) return { bytes, status: "unsupported", format: format2 };
1205
+ const parsed = parse(bytes, format2);
1206
+ if (!parsed) return { bytes, status: "unsupported", format: format2 };
1207
+ if (options.c2pa !== "overwrite" && parsed.c2pa) {
1208
+ return { bytes, status: "skipped-c2pa", format: format2 };
1209
+ }
1210
+ const marking = "sourceType" in input && "humanReviewed" in input ? input : createMarking(input);
1211
+ const xmp = buildXmp(marking, parsed.xmp);
1212
+ const out = format2 === "mp3" ? writeMp3(bytes, xmp) : format2 === "wav" ? writeWav(bytes, xmp) : writeMp4(bytes, xmp);
1213
+ if (!out) return { bytes, status: "unsupported", format: format2 };
1214
+ return { bytes: out, status: "marked", format: format2 };
1215
+ }
1216
+ function readMediaMarking(bytes) {
1217
+ const format2 = detectMediaFormat(bytes);
1218
+ const parsed = format2 ? parse(bytes, format2) : null;
1219
+ if (!format2 || !parsed) {
1220
+ return { format: format2, xmp: null, aiGenerated: false, witness: false, c2pa: false };
1221
+ }
1222
+ return describe(format2, parsed.xmp, parsed.c2pa);
1223
+ }
1224
+ function markFile(bytes, input = {}, options = {}) {
1225
+ if (detectImageFormat(bytes)) return markImage(bytes, input, options);
1226
+ return markMedia(bytes, input, options);
1227
+ }
1228
+ function readMarking(bytes) {
1229
+ if (detectImageFormat(bytes)) return readImageMarking(bytes);
1230
+ return readMediaMarking(bytes);
1231
+ }
1232
+ function describe(format2, xmp, c2pa) {
1233
+ const sourceType = xmp ? parseSourceType(xmpProperty(xmp, "Iptc4xmpExt:DigitalSourceType")) : void 0;
1234
+ const generator = xmp ? xmpProperty(xmp, "Iptc4xmpExt:AISystemUsed") ?? xmpProperty(xmp, "xmp:CreatorTool") : void 0;
1235
+ const info = {
1236
+ format: format2,
1237
+ xmp,
1238
+ aiGenerated: isAiSourceType(sourceType),
1239
+ witness: xmp?.includes(WITNESS_NS) ?? false,
1240
+ c2pa
1241
+ };
1242
+ if (sourceType) info.sourceType = sourceType;
1243
+ if (generator) info.generator = generator;
1244
+ return info;
1245
+ }
1246
+ function parse(bytes, format2) {
1247
+ if (format2 === "mp3") {
1248
+ const tag = readId3(bytes);
1249
+ if (tag === "unsupported") return null;
1250
+ if (!tag) return { xmp: null, c2pa: false };
1251
+ const xmpFrame = tag.frames.find((f) => f.id === "PRIV" && isXmpPriv(bytes, f));
1252
+ return {
1253
+ xmp: xmpFrame ? decode(bytes.subarray(xmpFrame.dataStart + 4, xmpFrame.end)) : null,
1254
+ c2pa: tag.frames.some(
1255
+ (f) => f.id === "GEOB" && ascii2(bytes, f.dataStart, Math.min(64, f.end - f.dataStart)).toLowerCase().includes("c2pa")
1256
+ )
1257
+ };
1258
+ }
1259
+ if (format2 === "wav") {
1260
+ const chunks = riffChunks(bytes);
1261
+ if (!chunks) return null;
1262
+ const xmp2 = chunks.find((c) => c.fourcc === "_PMX");
1263
+ return {
1264
+ xmp: xmp2 ? decode(bytes.subarray(xmp2.dataStart, xmp2.dataEnd)) : null,
1265
+ c2pa: chunks.some((c) => c.fourcc === "C2PA")
1266
+ };
1267
+ }
1268
+ const boxes = mp4Boxes(bytes);
1269
+ if (!boxes) return null;
1270
+ const xmp = boxes.find((b) => isUuidBox(bytes, b, XMP_UUID));
1271
+ return {
1272
+ xmp: xmp ? decode(bytes.subarray(xmp.dataStart + 16, xmp.end)) : null,
1273
+ c2pa: boxes.some((b) => isUuidBox(bytes, b, C2PA_UUID))
1274
+ };
1275
+ }
1276
+ function readId3(bytes) {
1277
+ if (ascii2(bytes, 0, 3) !== "ID3") return null;
1278
+ const version = bytes[3];
1279
+ const flags = bytes[5] ?? 0;
1280
+ if (version !== 3 && version !== 4) return "unsupported";
1281
+ if (flags & 128 || flags & 16) return "unsupported";
1282
+ const length = 10 + syncsafe(bytes, 6);
1283
+ if (length > bytes.length) return "unsupported";
1284
+ let offset = 10;
1285
+ if (flags & 64) {
1286
+ offset += version === 4 ? syncsafe(bytes, 10) : 4 + readU32BE2(bytes, 10);
1287
+ }
1288
+ const frames = [];
1289
+ while (offset + 10 <= length) {
1290
+ const id = ascii2(bytes, offset, 4);
1291
+ if (!/^[A-Z0-9]{4}$/.test(id)) break;
1292
+ const size = version === 4 ? syncsafe(bytes, offset + 4) : readU32BE2(bytes, offset + 4);
1293
+ const end = offset + 10 + size;
1294
+ if (end > length) return "unsupported";
1295
+ frames.push({ id, start: offset, dataStart: offset + 10, end });
1296
+ offset = end;
1297
+ }
1298
+ return { version, length, frames };
1299
+ }
1300
+ function isXmpPriv(bytes, frame) {
1301
+ return ascii2(bytes, frame.dataStart, 4) === "XMP\0";
1302
+ }
1303
+ function writeMp3(bytes, xmp) {
1304
+ const tag = readId3(bytes);
1305
+ if (tag === "unsupported") return null;
1306
+ const version = tag ? tag.version : 4;
1307
+ const kept = tag ? tag.frames.filter((f) => !(f.id === "PRIV" && isXmpPriv(bytes, f))).map((f) => bytes.subarray(f.start, f.end)) : [];
1308
+ const data = concat2(latin12("XMP\0"), utf82(xmp));
1309
+ const frame = new Uint8Array(10 + data.length);
1310
+ frame.set(latin12("PRIV"), 0);
1311
+ if (version === 4) writeSyncsafe(frame, 4, data.length);
1312
+ else writeU32BE2(frame, 4, data.length);
1313
+ frame.set(data, 10);
1314
+ const body = concat2(...kept, frame, new Uint8Array(64));
1315
+ const header = new Uint8Array(10);
1316
+ header.set(latin12("ID3"), 0);
1317
+ header[3] = version;
1318
+ writeSyncsafe(header, 6, body.length);
1319
+ return concat2(header, body, bytes.subarray(tag ? tag.length : 0));
1320
+ }
1321
+ function isMpegAudioFrame(bytes, offset) {
1322
+ const b1 = bytes[offset + 1] ?? 0;
1323
+ return bytes[offset] === 255 && (b1 & 224) === 224 && (b1 & 24) !== 8 && (b1 & 6) !== 0;
1324
+ }
1325
+ function syncsafe(b, o) {
1326
+ return ((b[o] ?? 0) & 127) << 21 | ((b[o + 1] ?? 0) & 127) << 14 | ((b[o + 2] ?? 0) & 127) << 7 | (b[o + 3] ?? 0) & 127;
1327
+ }
1328
+ function writeSyncsafe(b, o, v) {
1329
+ b[o] = v >>> 21 & 127;
1330
+ b[o + 1] = v >>> 14 & 127;
1331
+ b[o + 2] = v >>> 7 & 127;
1332
+ b[o + 3] = v & 127;
1333
+ }
1334
+ function riffChunks(bytes) {
1335
+ const chunks = [];
1336
+ const limit = Math.min(bytes.length, 8 + readU32LE2(bytes, 4));
1337
+ let offset = 12;
1338
+ while (offset + 8 <= limit) {
1339
+ const size = readU32LE2(bytes, offset + 4);
1340
+ const dataEnd = offset + 8 + size;
1341
+ if (dataEnd > bytes.length) return null;
1342
+ const end = Math.min(dataEnd + size % 2, bytes.length);
1343
+ chunks.push({
1344
+ fourcc: ascii2(bytes, offset, 4),
1345
+ start: offset,
1346
+ dataStart: offset + 8,
1347
+ dataEnd,
1348
+ end
1349
+ });
1350
+ offset = end;
1351
+ }
1352
+ return chunks;
1353
+ }
1354
+ function writeWav(bytes, xmp) {
1355
+ const chunks = riffChunks(bytes);
1356
+ if (!chunks) return null;
1357
+ const data = utf82(xmp);
1358
+ const chunk = new Uint8Array(8 + data.length + data.length % 2);
1359
+ chunk.set(latin12("_PMX"), 0);
1360
+ writeU32LE2(chunk, 4, data.length);
1361
+ chunk.set(data, 8);
1362
+ const body = concat2(
1363
+ ...chunks.filter((c) => c.fourcc !== "_PMX").map((c) => bytes.subarray(c.start, c.end)),
1364
+ chunk
1365
+ );
1366
+ const out = concat2(bytes.subarray(0, 12), body);
1367
+ writeU32LE2(out, 4, out.length - 8);
1368
+ return out;
1369
+ }
1370
+ function mp4Boxes(bytes) {
1371
+ const boxes = [];
1372
+ let offset = 0;
1373
+ while (offset + 8 <= bytes.length) {
1374
+ const size = readU32BE2(bytes, offset);
1375
+ const type = ascii2(bytes, offset + 4, 4);
1376
+ let header = 8;
1377
+ let end;
1378
+ if (size === 1) {
1379
+ if (offset + 16 > bytes.length) return null;
1380
+ const high = readU32BE2(bytes, offset + 8);
1381
+ end = offset + high * 2 ** 32 + readU32BE2(bytes, offset + 12);
1382
+ header = 16;
1383
+ } else if (size === 0) {
1384
+ end = bytes.length;
1385
+ } else {
1386
+ end = offset + size;
1387
+ }
1388
+ if (end > bytes.length || end < offset + header) return null;
1389
+ boxes.push({ type, start: offset, dataStart: offset + header, end, open: size === 0 });
1390
+ offset = end;
1391
+ }
1392
+ return boxes;
1393
+ }
1394
+ function isUuidBox(bytes, box, uuid) {
1395
+ if (box.type !== "uuid" || box.end - box.dataStart < 16) return false;
1396
+ for (let i = 0; i < 16; i++) if (bytes[box.dataStart + i] !== uuid[i]) return false;
1397
+ return true;
1398
+ }
1399
+ function writeMp4(bytes, xmp) {
1400
+ const boxes = mp4Boxes(bytes);
1401
+ if (!boxes) return null;
1402
+ const last = boxes[boxes.length - 1];
1403
+ let head = bytes;
1404
+ let length = bytes.length;
1405
+ if (last && isUuidBox(bytes, last, XMP_UUID)) length = last.start;
1406
+ head = bytes.slice(0, length);
1407
+ for (const box2 of boxes) {
1408
+ if (box2.start < length && isUuidBox(bytes, box2, XMP_UUID)) {
1409
+ head.set(latin12("free"), box2.start + 4);
1410
+ }
1411
+ }
1412
+ const open = boxes.find((b) => b.open && b.start < length);
1413
+ if (open) {
1414
+ const size = length - open.start;
1415
+ if (size > 4294967295) return null;
1416
+ writeU32BE2(head, open.start, size);
1417
+ }
1418
+ const data = utf82(xmp);
1419
+ const box = new Uint8Array(8 + 16 + data.length);
1420
+ writeU32BE2(box, 0, box.length);
1421
+ box.set(latin12("uuid"), 4);
1422
+ box.set(XMP_UUID, 8);
1423
+ box.set(data, 24);
1424
+ return concat2(head, box);
1425
+ }
1426
+ function hex(value) {
1427
+ const out = new Uint8Array(value.length / 2);
1428
+ for (let i = 0; i < out.length; i++) out[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16);
1429
+ return out;
1430
+ }
1431
+ function decode(bytes) {
1432
+ return new TextDecoder().decode(bytes).replace(/\0+$/, "");
1433
+ }
1434
+ function ascii2(bytes, offset, length) {
1435
+ let out = "";
1436
+ for (let i = offset; i < offset + length && i < bytes.length; i++) {
1437
+ out += String.fromCharCode(bytes[i] ?? 0);
1438
+ }
1439
+ return out;
1440
+ }
1441
+ function latin12(text) {
1442
+ const out = new Uint8Array(text.length);
1443
+ for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 255;
1444
+ return out;
1445
+ }
1446
+ function utf82(text) {
1447
+ return new TextEncoder().encode(text);
1448
+ }
1449
+ function concat2(...parts) {
1450
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
1451
+ let offset = 0;
1452
+ for (const part of parts) {
1453
+ out.set(part, offset);
1454
+ offset += part.length;
1455
+ }
1456
+ return out;
1457
+ }
1458
+ function readU32BE2(b, o) {
1459
+ return ((b[o] ?? 0) << 24 | (b[o + 1] ?? 0) << 16 | (b[o + 2] ?? 0) << 8 | (b[o + 3] ?? 0)) >>> 0;
1460
+ }
1461
+ function readU32LE2(b, o) {
1462
+ return ((b[o] ?? 0) | (b[o + 1] ?? 0) << 8 | (b[o + 2] ?? 0) << 16 | (b[o + 3] ?? 0) << 24) >>> 0;
1463
+ }
1464
+ function writeU32BE2(b, o, v) {
1465
+ b[o] = v >>> 24 & 255;
1466
+ b[o + 1] = v >>> 16 & 255;
1467
+ b[o + 2] = v >>> 8 & 255;
1468
+ b[o + 3] = v & 255;
1469
+ }
1470
+ function writeU32LE2(b, o, v) {
1471
+ b[o] = v & 255;
1472
+ b[o + 1] = v >>> 8 & 255;
1473
+ b[o + 2] = v >>> 16 & 255;
1474
+ b[o + 3] = v >>> 24 & 255;
1475
+ }
1476
+
1185
1477
  // src/types.ts
1186
1478
  var DISCLOSURE_KINDS = [
1187
1479
  "chatbot",
@@ -1312,6 +1604,7 @@ function findInRun(bytes) {
1312
1604
  createDisclosure,
1313
1605
  createMarking,
1314
1606
  detectImageFormat,
1607
+ detectMediaFormat,
1315
1608
  disclosureText,
1316
1609
  escapeHtml,
1317
1610
  format,
@@ -1322,7 +1615,9 @@ function findInRun(bytes) {
1322
1615
  isAiSourceType,
1323
1616
  labelHtml,
1324
1617
  labelText,
1618
+ markFile,
1325
1619
  markImage,
1620
+ markMedia,
1326
1621
  markingAttributes,
1327
1622
  markingJsonLd,
1328
1623
  markingMetaTags,
@@ -1330,6 +1625,8 @@ function findInRun(bytes) {
1330
1625
  nextMetadata,
1331
1626
  parseSourceType,
1332
1627
  readImageMarking,
1628
+ readMarking,
1629
+ readMediaMarking,
1333
1630
  readTextWatermark,
1334
1631
  registerLocale,
1335
1632
  registeredLocales,