Handle stds:exif C2PA assertions
This commit is contained in:
parent
ed79b3186a
commit
5c559a98ad
3 changed files with 128 additions and 66 deletions
|
|
@ -97,38 +97,6 @@ const updateDetails = () => {
|
||||||
return deg;
|
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
|
// Location
|
||||||
try {
|
try {
|
||||||
const lat = toDegrees(getSimpleValue("GPSLatitude"), getSimpleValue("GPSLatitudeRef") ?? "");
|
const lat = toDegrees(getSimpleValue("GPSLatitude"), getSimpleValue("GPSLatitudeRef") ?? "");
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import utils from "@/plugins/utils";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
export type AIInferenceResult = {
|
export type AIInferenceResult = {
|
||||||
aiGenerated: boolean;
|
aiGenerated: boolean;
|
||||||
aiProbability: number;
|
aiProbability: number;
|
||||||
|
|
@ -61,7 +64,6 @@ export type Proof = {
|
||||||
|
|
||||||
export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
||||||
export type ProofHintFlagSource = "c2pa" | "exif" | "metadata";
|
export type ProofHintFlagSource = "c2pa" | "exif" | "metadata";
|
||||||
//export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
|
||||||
|
|
||||||
export type ProofHintFlagsEdit = {
|
export type ProofHintFlagsEdit = {
|
||||||
editor: string;
|
editor: string;
|
||||||
|
|
@ -101,6 +103,66 @@ type FlagMatchInfo = {
|
||||||
re: string;
|
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[] => {
|
const ruleScreenshotC2PA = (): FlagMatchRule[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -245,7 +307,8 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
|
||||||
opart = opart.filter((item) => item[prop] !== val);
|
opart = opart.filter((item) => item[prop] !== val);
|
||||||
} else {
|
} else {
|
||||||
const [prop, val] = optionalConstraint.split("=");
|
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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFirstWithData = (flagPaths: string[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
||||||
for (let idx = 0; idx < flagPaths.length; idx++) {
|
let first: FlagMatchRuleValue | undefined = undefined;
|
||||||
const result = extractFlagValues(flagPaths[idx], path);
|
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) {
|
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;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFirstWithDataAsDate = (flagPaths: string[], path: FlagMatchRulePathSegment[]): Date | undefined => {
|
const getFirstWithDataAsDate = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): Date | undefined => {
|
||||||
const val = getFirstWithData(flagPaths, path);
|
let val = getFirstWithData(flagValues, path);
|
||||||
if (val) {
|
if (val) {
|
||||||
try {
|
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;
|
return date;
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
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 => {
|
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
|
||||||
if (!proof) return undefined;
|
if (!proof) return undefined;
|
||||||
|
|
||||||
|
|
@ -337,22 +411,16 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
|
||||||
let dateCreated: Date | undefined = undefined;
|
let dateCreated: Date | undefined = undefined;
|
||||||
let dateCreatedSource: ProofHintFlagSource | undefined = undefined;
|
let dateCreatedSource: ProofHintFlagSource | undefined = undefined;
|
||||||
|
|
||||||
let creationAction = extractFlagValues(
|
source = getFirstWithData(pathsC2PASource(), rootMatchPath);
|
||||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]",
|
dateCreated = getFirstWithDataAsDate(pathsC2PACreationDate(), rootMatchPath);
|
||||||
rootMatchPath
|
if (dateCreated) {
|
||||||
);
|
|
||||||
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);
|
|
||||||
dateCreatedSource = "c2pa";
|
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 generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), rootMatchPath).result ? "ai" : matchFlag(ruleScreenshotC2PA(), rootMatchPath).result ? "screenshot" : matchFlag(ruleCamera(), rootMatchPath).result ? "camera" : "unknown";
|
||||||
let generatorSource: ProofHintFlagSource | undefined = undefined;
|
let generatorSource: ProofHintFlagSource | undefined = undefined;
|
||||||
|
|
@ -378,8 +446,16 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
|
||||||
if (c2paEdits.length > 0) {
|
if (c2paEdits.length > 0) {
|
||||||
edits = c2paEdits.map((edit) => {
|
edits = c2paEdits.map((edit) => {
|
||||||
return {
|
return {
|
||||||
editor: softwareAgentFromAction(edit) ?? "",
|
editor: getFirstWithData([
|
||||||
date: getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], edit.path)
|
{path: "softwareAgent/name"},
|
||||||
|
{path: "softwareAgent"},
|
||||||
|
{path: "../../../claim_generator"}
|
||||||
|
], edit.path) ?? "",
|
||||||
|
date: getFirstWithDataAsDate([
|
||||||
|
{path: "when"},
|
||||||
|
{path: "../../metadata/dateTime"},
|
||||||
|
{path: "../../../signature_info/time"}
|
||||||
|
], edit.path)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1040,6 +1040,24 @@ class Util {
|
||||||
return then.format("lll");
|
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() {
|
browserCanRecordAudio() {
|
||||||
return _browserCanRecordAudio;
|
return _browserCanRecordAudio;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue