From f87446c6a4c9489782d6c90746f1e05ac4359f56 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Wed, 12 Nov 2025 15:20:31 +0100 Subject: [PATCH] Cleanup metadata code and detect Mac screenshots via UserData Exif field. --- src/models/proof.ts | 179 ++++++++++++++++++++++++-------------------- 1 file changed, 99 insertions(+), 80 deletions(-) diff --git a/src/models/proof.ts b/src/models/proof.ts index c2d4aef..78642ae 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -121,6 +121,8 @@ type FlagMatchInfo = { type FlagValue = { path: string; transform?: (value: any, match: FlagMatchRuleValue) => any; + matches?: string[]; + description?: string; // Not currently used }; const pathsC2PASource = (): FlagValue[] => { @@ -176,9 +178,7 @@ const pathsC2PACreationDate = (): FlagValue[] => { const pathsC2PACamera = (): FlagValue[] => { return [ - { - path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data{exif:SceneType=1|directly photographed image}", - }, + { path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:SceneType", matches: ["^1$", "^directly photographed image$"] }, { path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensMake" }, { path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensModel" }, ]; @@ -186,12 +186,31 @@ const pathsC2PACamera = (): FlagValue[] => { const pathsExifCamera = (): FlagValue[] => { return [ - { path: "integrity/exif{SceneType=1|directly photographed image}" }, + { path: "integrity/exif/SceneType", matches: ["^1$", "^directly photographed image$"] }, { path: "integrity/exif/LensMake" }, { path: "integrity/exif/LensModel" }, ]; }; +const pathsExifScreenshot = (): FlagValue[] => { + return [ + { + path: "integrity/exif/UserComment", + transform: exifStringTransform, + matches: ["^Screenshot$"], + }, + ]; +}; + +const pathsMetaScreenshot = (): FlagValue[] => { +return [ + { + path: "name", + matches: ["screenshot"], + } + ]; +}; + const pathsC2PALocation = (): FlagValue[] => { return [ { path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLatitude" }, @@ -210,6 +229,30 @@ const pathsExifLocation = (): FlagValue[] => { ]; }; +const pathsMetaAI = (): FlagValue[] => { + const knownAIServices = [ + "ChatGPT", + "OpenAI-API", + "Adobe Firefly", + "RunwayML", + "Runway AI", + "Google AI", + "Stable Diffusion" + ]; + return [ + { path: "name", matches: ["^DALL_E_", "^Gen-3"], description: "File name" }, + { + path: "integrity/c2pa/manifest_info/manifests[]/claim_generator", + matches: knownAIServices, + description: "C2PA claim generator", + }, + { path: "iptc/Credit", matches: knownAIServices, description: "IPTC Credit" }, + { path: "iptc/Provider", matches: knownAIServices, description: "IPTC Provider" }, + { path: "iptc/ImageSupplier[]", matches: knownAIServices, description: "IPTC ImageSupplier" }, + { path: "iptc/ImageCreator[]", matches: knownAIServices, description: "IPTC ImageCreator" }, + ]; +}; + const getExifValue = (val: string): string => { return val.replace(/^"(.+(?="$))"$/, "$1"); }; @@ -249,6 +292,34 @@ const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue): return getExifMakeModel(match, "exif:"); }; +const exifStringTransform = (value: any, match: FlagMatchRuleValue): string => { + try { + const s = value as string; + if (s) { + if (s.toLowerCase().startsWith("0x")) { + if (s.toLowerCase().startsWith("0x554e49434f444500")) { + // Unicode + const buffer = Buffer.from(s.substring(18), "hex"); + return buffer.toString("utf-8"); + // } else if (s.toLowerCase().startsWith("0x4a49530000000000")) { + // // JIS + // const buffer = Buffer.from(s.substring(18), 'hex'); + // return buffer.toString("ascii"); + } else if ( + s.toLowerCase().startsWith("0x4153434949000000") || + s.toLowerCase().startsWith("0x0000000000000000") + ) { + // Ascii + const buffer = Buffer.from(s.substring(18), "hex"); + console.log(buffer.toString("ascii")); + return buffer.toString("ascii"); + } + } + } + } catch (e) {} + return value; +}; + const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => { // Make and model let makeAndModel = ""; @@ -266,61 +337,6 @@ const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => return makeAndModel; }; -const ruleScreenshotMeta = (): FlagMatchRule[] => { - return [ - { - field: "name", - match: ["screenshot"], - description: "Screen capture", - }, - ]; -}; - -const ruleAiMeta = (): FlagMatchRule[] => { - const knownAIServices = [ - "ChatGPT", - "OpenAI-API", - "Adobe Firefly", - "RunwayML", - "Runway AI", - "Google AI", - "Stable Diffusion", - ]; - return [ - { field: "name", match: ["^DALL_E_", "^Gen-3"], description: "File name" }, - { - field: "integrity/c2pa/manifest_info/manifests[]/claim_generator", - match: knownAIServices, - description: "C2PA claim generator", - }, - { field: "iptc/Credit", match: knownAIServices, description: "IPTC Credit" }, - { field: "iptc/Provider", match: knownAIServices, description: "IPTC Provider" }, - { field: "iptc/ImageSupplier[]", match: knownAIServices, description: "IPTC ImageSupplier" }, - { field: "iptc/ImageCreator[]", match: knownAIServices, description: "IPTC ImageCreator" }, - ]; -}; - -const matchFlag = (rules: FlagMatchRule[], path: FlagMatchRulePathSegment[]) => { - let result = false; - let resultInfo: FlagMatchInfo[] = []; - for (let rule of rules) { - try { - const re: RegExp[] = (!Array.isArray(rule.match) ? [rule.match] : rule.match).map((m) => new RegExp(m, "gi")); - const values = extractFlagValues(rule.field, path); - values.forEach((v) => { - re.forEach((r) => { - if (typeof v.value === "string" && r.test(v.value)) { - result = true; - resultInfo.push({ field: v.path.map((v) => v.path).join("/"), value: v.value, re: r.source }); - } - }); - }); - } catch (e) { - console.error("Invalid RE", e); - } - } - return { result: result, matches: resultInfo }; -}; const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => { const getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => { @@ -335,17 +351,12 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): const lastBracket = key.lastIndexOf("["); const hasBracket = key.endsWith("]") && lastBracket > 0; - const lastBrace = key.lastIndexOf("{"); - const hasBrace = key.endsWith("}") && lastBrace > 0; let optionalConstraint: String | undefined = undefined; if (hasBracket) { optionalConstraint = key.substring(lastBracket + 1, key.length - 1); key = key.substring(0, lastBracket); - } else if (hasBrace) { - optionalConstraint = key.substring(lastBrace + 1, key.length - 1); - key = key.substring(0, lastBrace); } let nextObject = o[key]; @@ -415,25 +426,30 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): }; const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): string | undefined => { - let first: FlagMatchRuleValue | undefined = undefined; - let transform: ((value: any, match: FlagMatchRuleValue) => any) | undefined = undefined; for (let idx = 0; idx < flagValues.length; idx++) { const result = extractFlagValues(flagValues[idx].path, path); if (result.length > 0) { - first = result[0]; - transform = flagValues[idx].transform; - break; + try { + let first = result[0]; + let val: string | undefined = first.value as string; + let transform = flagValues[idx].transform; + if (val && transform) { + val = transform(val, first); + } + if (val && flagValues[idx].matches) { + if (!flagValues[idx].matches!.some((m) => { + const re = new RegExp(m, "gi"); + return re.test(val!); + })) { + val = undefined; + } + } + if (val) { + return val; + } + } catch (error) {} } } - if (first && (first.value as string)) { - try { - let val = first.value as string; - if (val && transform) { - val = transform(val, first); - } - return val; - } catch (error) {} - } return undefined; }; @@ -538,10 +554,13 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined = } else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) { generator = "camera"; generatorSource = "exif"; - } else if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) { + } else if (getFirstWithData(pathsExifScreenshot(), rootMatchPath)) { + generator = "screenshot"; + generatorSource = "exif"; + } else if (getFirstWithData(pathsMetaScreenshot(), rootMatchPath)) { generator = "screenshot"; generatorSource = "metadata"; - } else if (matchFlag(ruleAiMeta(), rootMatchPath).result) { + } else if (getFirstWithData(pathsMetaAI(), rootMatchPath)) { generator = "ai"; generatorSource = "metadata"; }