Handle stds:exif C2PA assertions

This commit is contained in:
N-Pex 2025-10-22 15:03:56 +02:00
parent ed79b3186a
commit 5c559a98ad
3 changed files with 128 additions and 66 deletions

View file

@ -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") ?? "");

View file

@ -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)
}
});
}

View file

@ -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;
}