From 5c559a98ad0357875fbb166dabe71e057f7a9913 Mon Sep 17 00:00:00 2001 From: N-Pex Date: Wed, 22 Oct 2025 15:03:56 +0200 Subject: [PATCH] Handle stds:exif C2PA assertions --- .../content-credentials/C2PAInfo.vue | 32 ---- src/models/proof.ts | 144 +++++++++++++----- src/plugins/utils.js | 18 +++ 3 files changed, 128 insertions(+), 66 deletions(-) diff --git a/src/components/content-credentials/C2PAInfo.vue b/src/components/content-credentials/C2PAInfo.vue index 9c041d7..e24742e 100644 --- a/src/components/content-credentials/C2PAInfo.vue +++ b/src/components/content-credentials/C2PAInfo.vue @@ -97,38 +97,6 @@ const updateDetails = () => { return deg; }; - // Make and model - let makeAndModel = ""; - const make = getSimpleValue("Make"); - const model = getSimpleValue("Model"); - if (make) { - makeAndModel += make; - } - if (model) { - if (makeAndModel.length > 0) { - makeAndModel += ", "; - } - makeAndModel += model; - } - if (makeAndModel.length > 0) { - d.push({ - icon: "$vuetify.icons.ic_exif_device_camera", - title: t("file_mode.cc_source"), - value: makeAndModel, - }); - } - - // Creation date - const date = getSimpleValue("DateTimeOriginal"); - const dateOffset = getSimpleValue("OffsetTimeOriginal"); - if (date) { - d.push({ - icon: "$vuetify.icons.ic_exif_time", - title: t("file_mode.cc_capture_timestamp"), - value: dayjs(Date.parse(date + (dateOffset ? dateOffset : ""))).format("lll"), - }); - } - // Location try { const lat = toDegrees(getSimpleValue("GPSLatitude"), getSimpleValue("GPSLatitudeRef") ?? ""); diff --git a/src/models/proof.ts b/src/models/proof.ts index bc1697a..08cd2e6 100644 --- a/src/models/proof.ts +++ b/src/models/proof.ts @@ -1,3 +1,6 @@ +import utils from "@/plugins/utils"; +import dayjs from "dayjs"; + export type AIInferenceResult = { aiGenerated: boolean; aiProbability: number; @@ -61,7 +64,6 @@ export type Proof = { export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai"; export type ProofHintFlagSource = "c2pa" | "exif" | "metadata"; -//export type ProofHintFlagsEditor = "unknown" | "manual" | "ai"; export type ProofHintFlagsEdit = { editor: string; @@ -101,6 +103,66 @@ type FlagMatchInfo = { re: string; }; +type FlagValue = { + path: string; + transform?: (value: any, match: FlagMatchRuleValue) => any; +} + +const pathsC2PASource = (): FlagValue[] => { + return [ + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Make", transform: getExifMakeModel }, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Model", transform: getExifMakeModel }, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/softwareAgent/name"}, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/softwareAgent"}, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../../claim_generator"} + ]; +}; + +const pathsC2PACreationDate = (): FlagValue[] => { + return [ + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/when"}, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../metadata/dateTime"}, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../../signature_info/time"}, + {path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:DateTimeOriginal", transform: (value, match) => { + // Check if we have a timestamp offset + let offset = extractFlagValues("../exif:OffsetTimeOriginal", match.path)?.at(0)?.value; + return dayjs(Date.parse(value + (offset ? offset : ""))).format("lll"); + }} + ]; +} + +const getExifValue = (val: string): string => { + return val.replace(/^"(.+(?="$))"$/, "$1"); +}; + +const pathsExifCreationDate = (): FlagValue[] => { + return [ + {path: "integrity/exif/DateTimeOriginal", transform: (value, match) => { + let dateTimeOriginal = getExifValue(value); + // Check if we have a timestamp offset + let offset = extractFlagValues("../OffsetTimeOriginal", match.path)?.at(0)?.value as string; + return dayjs(Date.parse(dateTimeOriginal + (offset ? getExifValue(offset) : ""))).format("lll"); + }} + ]; +} + +const getExifMakeModel = (ignoredvalue: any, match: FlagMatchRuleValue): string => { + // Make and model + let makeAndModel = ""; + const make = extractFlagValues("../exif:Make", match.path)?.at(0)?.value as string; + const model = extractFlagValues("../exif:Model", match.path)?.at(0)?.value as string; + if (make) { + makeAndModel += make; + } + if (model) { + if (makeAndModel.length > 0) { + makeAndModel += ", "; + } + makeAndModel += model; + } + return makeAndModel; +} + const ruleScreenshotC2PA = (): FlagMatchRule[] => { return [ { @@ -245,7 +307,8 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): opart = opart.filter((item) => item[prop] !== val); } else { const [prop, val] = optionalConstraint.split("="); - opart = opart.filter((item) => item[prop] === val); + let valarray = val.split("|"); + opart = opart.filter((item) => valarray.includes(item[prop])); } } @@ -290,35 +353,46 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): return result; }; -const getFirstWithData = (flagPaths: string[], path: FlagMatchRulePathSegment[]): string | undefined => { - for (let idx = 0; idx < flagPaths.length; idx++) { - const result = extractFlagValues(flagPaths[idx], path); +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) { - return result[0].value as string; + first = result[0] + transform = flagValues[idx].transform; + break; + } + } + 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; }; -const getFirstWithDataAsDate = (flagPaths: string[], path: FlagMatchRulePathSegment[]): Date | undefined => { - const val = getFirstWithData(flagPaths, path); +const getFirstWithDataAsDate = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): Date | undefined => { + let val = getFirstWithData(flagValues, path); if (val) { try { - const date = new Date(Date.parse(val)); + let date = new Date(Date.parse(val)); + if (isNaN(date.valueOf())) { + // Try EXIF format + date = utils.parseExifDate(val); + } return date; - } catch (error) {} + } catch (error) { + } } return undefined; }; -const softwareAgentFromAction = (action: FlagMatchRuleValue): string | undefined => { - const agent = getFirstWithData([ - "softwareAgent/name", "softwareAgent", "../../../claim_generator" - ], action.path); - return agent; -}; - export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => { if (!proof) return undefined; @@ -337,22 +411,16 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined let dateCreated: Date | undefined = undefined; let dateCreatedSource: ProofHintFlagSource | undefined = undefined; - let creationAction = extractFlagValues( - "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]", - rootMatchPath - ); - if (creationAction.length == 0) { - creationAction = extractFlagValues( - "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions.v2]/data/actions[action=c2pa.created]", - rootMatchPath - ); - } - if (creationAction.length > 0) { - source = softwareAgentFromAction(creationAction[0]); - dateCreated = getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], creationAction[0].path); + source = getFirstWithData(pathsC2PASource(), rootMatchPath); + dateCreated = getFirstWithDataAsDate(pathsC2PACreationDate(), rootMatchPath); + if (dateCreated) { dateCreatedSource = "c2pa"; + } else { + dateCreated = getFirstWithDataAsDate(pathsExifCreationDate(), rootMatchPath); + if (dateCreated) { + dateCreatedSource = "exif"; + } } - console.log("DATE CREATED", dateCreated); let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), rootMatchPath).result ? "ai" : matchFlag(ruleScreenshotC2PA(), rootMatchPath).result ? "screenshot" : matchFlag(ruleCamera(), rootMatchPath).result ? "camera" : "unknown"; let generatorSource: ProofHintFlagSource | undefined = undefined; @@ -378,8 +446,16 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined if (c2paEdits.length > 0) { edits = c2paEdits.map((edit) => { return { - editor: softwareAgentFromAction(edit) ?? "", - date: getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], edit.path) + editor: getFirstWithData([ + {path: "softwareAgent/name"}, + {path: "softwareAgent"}, + {path: "../../../claim_generator"} + ], edit.path) ?? "", + date: getFirstWithDataAsDate([ + {path: "when"}, + {path: "../../metadata/dateTime"}, + {path: "../../../signature_info/time"} + ], edit.path) } }); } diff --git a/src/plugins/utils.js b/src/plugins/utils.js index 9089d4a..16e79e1 100644 --- a/src/plugins/utils.js +++ b/src/plugins/utils.js @@ -1040,6 +1040,24 @@ class Util { return then.format("lll"); } + parseExifDate(exifDateString) { + // Use a regular expression to match and capture the date/time components. + const regex = /(\d{4}):(\d{2}):(\d{2})\s(\d{2}):(\d{2}):(\d{2})/; + const match = exifDateString.match(regex); + + if (match) { + // Extract components and convert to numbers. Note that months are 0-indexed. + const year = parseInt(match[1], 10); + const month = parseInt(match[2], 10) - 1; + const day = parseInt(match[3], 10); + const hour = parseInt(match[4], 10); + const minute = parseInt(match[5], 10); + const second = parseInt(match[6], 10); + return new Date(year, month, day, hour, minute, second); + } + return undefined; + } + browserCanRecordAudio() { return _browserCanRecordAudio; }