2025-10-22 15:03:56 +02:00
|
|
|
import utils from "@/plugins/utils";
|
|
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
|
2025-07-04 12:51:57 +02:00
|
|
|
export type AIInferenceResult = {
|
|
|
|
|
aiGenerated: boolean;
|
|
|
|
|
aiProbability: number;
|
|
|
|
|
humanProbability: number;
|
2025-08-28 10:27:57 +02:00
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
2025-07-11 14:36:58 +02:00
|
|
|
export const C2PASourceTypeScreenCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture";
|
|
|
|
|
export const C2PASourceTypeDigitalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture";
|
|
|
|
|
export const C2PASourceTypeComputationalCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture";
|
|
|
|
|
export const C2PASourceTypeCompositeCapture = "http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture";
|
2025-08-28 10:27:57 +02:00
|
|
|
export const C2PASourceTypeTrainedAlgorithmicMedia =
|
|
|
|
|
"http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia";
|
|
|
|
|
export const C2PASourceTypeCompositeWithTrainedAlgorithmicMedia =
|
|
|
|
|
"http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia";
|
2025-07-11 14:36:58 +02:00
|
|
|
|
2025-07-04 12:51:57 +02:00
|
|
|
export type C2PAActionsAssertion = {
|
2025-08-28 10:27:57 +02:00
|
|
|
actions: {
|
|
|
|
|
action: string;
|
|
|
|
|
softwareAgent?: string;
|
|
|
|
|
digitalSourceType?: string;
|
|
|
|
|
}[];
|
|
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
|
|
|
|
export type C2PAAssertion = {
|
2025-08-28 10:27:57 +02:00
|
|
|
label: string;
|
|
|
|
|
data: C2PAActionsAssertion | undefined;
|
|
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
|
|
|
|
export type C2PAManifest = {
|
2025-08-28 10:27:57 +02:00
|
|
|
assertions: C2PAAssertion[];
|
|
|
|
|
signature_info: {
|
|
|
|
|
time: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
2025-07-11 14:36:58 +02:00
|
|
|
export type C2PAValidationResults = {
|
2025-08-28 10:27:57 +02:00
|
|
|
activeManifest?: {
|
|
|
|
|
failure: any[];
|
|
|
|
|
success: any[];
|
|
|
|
|
informational: any[];
|
|
|
|
|
};
|
|
|
|
|
};
|
2025-07-11 14:36:58 +02:00
|
|
|
|
2025-07-04 12:51:57 +02:00
|
|
|
export type C2PAManifestInfo = {
|
2025-08-28 10:27:57 +02:00
|
|
|
active_manifest: string;
|
|
|
|
|
manifests: { [key: string]: C2PAManifest };
|
|
|
|
|
validation_results?: C2PAValidationResults;
|
|
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
|
|
|
|
export type C2PAData = {
|
2025-08-28 10:27:57 +02:00
|
|
|
manifest_info: C2PAManifestInfo;
|
|
|
|
|
};
|
2025-07-04 12:51:57 +02:00
|
|
|
|
|
|
|
|
export type Proof = {
|
2025-08-28 10:27:57 +02:00
|
|
|
data?: any;
|
|
|
|
|
name?: string;
|
|
|
|
|
json?: string;
|
|
|
|
|
integrity?: { pgp?: any; c2pa?: C2PAData; exif?: { [key: string]: string | Object }; opentimestamps?: any };
|
|
|
|
|
ai?: { inferenceResult?: AIInferenceResult };
|
|
|
|
|
};
|
2025-08-20 15:12:04 +02:00
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
export type MediaMetadataGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
2025-10-23 10:13:14 +02:00
|
|
|
export type MediaMetadataPropertySource = "c2pa" | "exif" | "metadata";
|
2025-09-09 10:56:15 +02:00
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
export type MediaMetadataEdit = {
|
2025-09-23 12:52:52 +02:00
|
|
|
editor: string;
|
2025-09-09 10:56:15 +02:00
|
|
|
date?: Date;
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type MediaMetadataLocation = {
|
|
|
|
|
latitude: string;
|
|
|
|
|
longitude: string;
|
|
|
|
|
source: MediaMetadataPropertySource;
|
|
|
|
|
};
|
2025-09-09 10:56:15 +02:00
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
export type MediaInterventionFlags = {
|
|
|
|
|
creationDate?: Date;
|
|
|
|
|
generator?: MediaMetadataGenerator;
|
|
|
|
|
modified?: boolean;
|
|
|
|
|
containsC2PA?: boolean;
|
|
|
|
|
containsEXIF?: boolean;
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-10-23 10:13:14 +02:00
|
|
|
|
|
|
|
|
export type MediaMetadata = {
|
2025-09-09 10:56:15 +02:00
|
|
|
device?: string;
|
2025-09-05 11:16:50 +02:00
|
|
|
creationDate?: Date;
|
2025-10-23 10:13:14 +02:00
|
|
|
creationDateSource?: MediaMetadataPropertySource;
|
|
|
|
|
generator?: MediaMetadataGenerator;
|
|
|
|
|
generatorSource?: MediaMetadataPropertySource;
|
|
|
|
|
edits?: MediaMetadataEdit[];
|
2025-09-23 16:10:28 +02:00
|
|
|
containsC2PA?: boolean;
|
|
|
|
|
containsEXIF?: boolean;
|
2025-11-03 15:27:42 +01:00
|
|
|
location?: MediaMetadataLocation;
|
2025-08-28 10:27:57 +02:00
|
|
|
};
|
2025-08-20 15:12:04 +02:00
|
|
|
|
2025-08-28 10:27:57 +02:00
|
|
|
type FlagMatchRule = {
|
|
|
|
|
field: string;
|
|
|
|
|
match: string[];
|
|
|
|
|
description: string;
|
|
|
|
|
};
|
2025-08-20 15:12:04 +02:00
|
|
|
|
2025-09-23 12:52:52 +02:00
|
|
|
type FlagMatchRulePathSegment = {
|
|
|
|
|
object: any;
|
2025-09-01 16:26:05 +02:00
|
|
|
path: string;
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-09-23 12:52:52 +02:00
|
|
|
|
|
|
|
|
type FlagMatchRuleValue = {
|
|
|
|
|
path: FlagMatchRulePathSegment[];
|
|
|
|
|
value: string | object;
|
2025-09-01 16:26:05 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-28 10:27:57 +02:00
|
|
|
type FlagMatchInfo = {
|
|
|
|
|
field: string;
|
|
|
|
|
value: string;
|
|
|
|
|
re: string;
|
|
|
|
|
};
|
2025-08-25 14:47:12 +02:00
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
type FlagValue = {
|
|
|
|
|
path: string;
|
|
|
|
|
transform?: (value: any, match: FlagMatchRuleValue) => any;
|
2025-11-12 15:20:31 +01:00
|
|
|
matches?: string[];
|
|
|
|
|
description?: string; // Not currently used
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-10-22 15:03:56 +02:00
|
|
|
|
|
|
|
|
const pathsC2PASource = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-03 15:27:42 +01:00
|
|
|
{
|
|
|
|
|
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Make",
|
2025-11-14 13:00:06 +01:00
|
|
|
transform: getMakeModel,
|
2025-11-03 15:27:42 +01:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Model",
|
2025-11-14 13:00:06 +01:00
|
|
|
transform: getMakeModel,
|
2025-11-03 15:27:42 +01:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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",
|
|
|
|
|
},
|
2025-11-14 13:00:06 +01:00
|
|
|
{
|
|
|
|
|
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/tiff:Make",
|
|
|
|
|
transform: getMakeModel,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/tiff:Model",
|
|
|
|
|
transform: getMakeModel,
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
{
|
|
|
|
|
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/../../../claim_generator",
|
|
|
|
|
},
|
2025-10-22 15:03:56 +02:00
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-23 07:45:39 +02:00
|
|
|
const pathsExifSource = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-14 13:00:06 +01:00
|
|
|
{ path: "integrity/exif/Make", transform: getMakeModel },
|
|
|
|
|
{ path: "integrity/exif/Model", transform: getMakeModel },
|
2025-10-23 07:45:39 +02:00
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
const pathsC2PACreationDate = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-03 15:27:42 +01:00
|
|
|
{
|
|
|
|
|
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) => {
|
2025-10-22 15:03:56 +02:00
|
|
|
// 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");
|
2025-11-03 15:27:42 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-10-22 15:03:56 +02:00
|
|
|
];
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-10-22 15:03:56 +02:00
|
|
|
|
2025-10-23 07:45:39 +02:00
|
|
|
const pathsC2PACamera = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-12 15:20:31 +01:00
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:SceneType", matches: ["^1$", "^directly photographed image$"] },
|
2025-11-03 15:27:42 +01:00
|
|
|
{ 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" },
|
2025-11-14 13:00:06 +01:00
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exifEX:LensMake" },
|
|
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exifEX:LensModel" },
|
2025-10-23 07:45:39 +02:00
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pathsExifCamera = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-12 15:20:31 +01:00
|
|
|
{ path: "integrity/exif/SceneType", matches: ["^1$", "^directly photographed image$"] },
|
2025-11-03 15:27:42 +01:00
|
|
|
{ path: "integrity/exif/LensMake" },
|
|
|
|
|
{ path: "integrity/exif/LensModel" },
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-12 15:20:31 +01:00
|
|
|
const pathsExifScreenshot = (): FlagValue[] => {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
path: "integrity/exif/UserComment",
|
|
|
|
|
transform: exifStringTransform,
|
|
|
|
|
matches: ["^Screenshot$"],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pathsMetaScreenshot = (): FlagValue[] => {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
path: "name",
|
|
|
|
|
matches: ["screenshot"],
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
const pathsC2PALocation = (): FlagValue[] => {
|
|
|
|
|
return [
|
|
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLatitude" },
|
|
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLongitude" },
|
|
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLatitudeRef" },
|
|
|
|
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLongitudeRef" },
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pathsExifLocation = (): FlagValue[] => {
|
|
|
|
|
return [
|
|
|
|
|
{ path: "integrity/exif/GPSLatitude" },
|
|
|
|
|
{ path: "integrity/exif/GPSLongitude" },
|
|
|
|
|
{ path: "integrity/exif/GPSLatitudeRef" },
|
|
|
|
|
{ path: "integrity/exif/GPSLongitudeRef" },
|
2025-10-23 07:45:39 +02:00
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-12 15:20:31 +01:00
|
|
|
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" },
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
const getExifValue = (val: string): string => {
|
|
|
|
|
return val.replace(/^"(.+(?="$))"$/, "$1");
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
const toDegrees = (dms: string | undefined, direction: string) => {
|
|
|
|
|
if (!dms || dms.length == 0) return undefined;
|
|
|
|
|
var parts = dms.split(/deg|min|sec/);
|
|
|
|
|
var d = parts[0];
|
|
|
|
|
var m = parts[1];
|
|
|
|
|
var s = parts[2];
|
|
|
|
|
var deg = (Number(d) + Number(m) / 60 + Number(s) / 3600).toFixed(6);
|
|
|
|
|
if (direction == "S" || direction == "W") {
|
|
|
|
|
deg = "-" + deg;
|
|
|
|
|
}
|
|
|
|
|
return deg;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
const pathsExifCreationDate = (): FlagValue[] => {
|
|
|
|
|
return [
|
2025-11-03 15:27:42 +01:00
|
|
|
{
|
|
|
|
|
path: "integrity/exif/DateTimeOriginal",
|
|
|
|
|
transform: (value, match) => {
|
2025-10-22 15:03:56 +02:00
|
|
|
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");
|
2025-11-03 15:27:42 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-10-22 15:03:56 +02:00
|
|
|
];
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-10-22 15:03:56 +02:00
|
|
|
|
2025-11-14 13:00:06 +01:00
|
|
|
const getMakeModel = (ignoredval: string, match: FlagMatchRuleValue): string => {
|
|
|
|
|
let prefix: string = "";
|
|
|
|
|
|
|
|
|
|
const pathLeaf = match.path.at(0)?.path;
|
|
|
|
|
if (pathLeaf && pathLeaf.indexOf(":") >= 0) {
|
|
|
|
|
prefix = pathLeaf.substring(0, pathLeaf.indexOf(":") + 1);
|
|
|
|
|
}
|
2025-10-23 07:45:39 +02:00
|
|
|
|
2025-11-14 13:00:06 +01:00
|
|
|
// Make and model
|
|
|
|
|
let makeAndModel = "";
|
|
|
|
|
const make = extractFlagValues(`../${prefix}Make`, match.path)?.at(0)?.value as string;
|
|
|
|
|
const model = extractFlagValues(`../${prefix}Model`, match.path)?.at(0)?.value as string;
|
|
|
|
|
if (make) {
|
|
|
|
|
makeAndModel += getExifValue(make);
|
|
|
|
|
}
|
|
|
|
|
if (model) {
|
|
|
|
|
if (makeAndModel.length > 0) {
|
|
|
|
|
makeAndModel += ", ";
|
|
|
|
|
}
|
|
|
|
|
makeAndModel += getExifValue(model);
|
|
|
|
|
}
|
|
|
|
|
return makeAndModel;
|
2025-11-03 15:27:42 +01:00
|
|
|
};
|
2025-10-23 07:45:39 +02:00
|
|
|
|
2025-11-12 15:20:31 +01:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-23 12:52:52 +02:00
|
|
|
const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
|
2025-11-03 15:27:42 +01:00
|
|
|
const getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => {
|
2025-09-23 12:52:52 +02:00
|
|
|
if (keys.length == 0 || path.length == 0) return undefined;
|
2025-11-03 09:17:09 +01:00
|
|
|
|
2025-09-23 12:52:52 +02:00
|
|
|
const o = path[0].object;
|
|
|
|
|
let key = keys[0];
|
|
|
|
|
|
|
|
|
|
if (key === "..") {
|
|
|
|
|
return getValues(keys.slice(1), path.slice(1));
|
2025-09-01 16:26:05 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-03 09:17:09 +01:00
|
|
|
const lastBracket = key.lastIndexOf("[");
|
|
|
|
|
const hasBracket = key.endsWith("]") && lastBracket > 0;
|
|
|
|
|
|
|
|
|
|
let optionalConstraint: String | undefined = undefined;
|
|
|
|
|
|
|
|
|
|
if (hasBracket) {
|
|
|
|
|
optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
|
|
|
|
key = key.substring(0, lastBracket);
|
|
|
|
|
}
|
2025-09-01 16:26:05 +02:00
|
|
|
|
2025-11-03 09:17:09 +01:00
|
|
|
let nextObject = o[key];
|
2025-11-03 15:27:42 +01:00
|
|
|
let omatches: any[] = nextObject
|
|
|
|
|
? Array.isArray(nextObject)
|
|
|
|
|
? nextObject
|
|
|
|
|
: hasBracket
|
|
|
|
|
? Object.values(nextObject)
|
|
|
|
|
: [nextObject]
|
|
|
|
|
: [];
|
2025-11-03 09:17:09 +01:00
|
|
|
|
|
|
|
|
// Any constraints controlling what array object(s) to consider?
|
|
|
|
|
if (optionalConstraint) {
|
|
|
|
|
if (optionalConstraint.startsWith("!!")) {
|
|
|
|
|
// Ignore this path in the tree if ANY of the values contain the constraint match.
|
|
|
|
|
const [prop, val] = optionalConstraint.substring(2).split("=");
|
|
|
|
|
let valarray = val.split("|");
|
|
|
|
|
if (omatches.some((item) => valarray.includes(item[prop]))) {
|
|
|
|
|
omatches = [];
|
2025-09-01 16:26:05 +02:00
|
|
|
}
|
2025-11-03 09:17:09 +01:00
|
|
|
} else if (optionalConstraint.startsWith("!")) {
|
|
|
|
|
const [prop, val] = optionalConstraint.substring(1).split("=");
|
|
|
|
|
let valarray = val.split("|");
|
|
|
|
|
omatches = omatches.filter((m) => {
|
|
|
|
|
return valarray.includes(m[prop]);
|
|
|
|
|
});
|
2025-09-01 16:26:05 +02:00
|
|
|
} else {
|
2025-11-03 09:17:09 +01:00
|
|
|
const [prop, val] = optionalConstraint.split("=");
|
|
|
|
|
let valarray = val.split("|");
|
|
|
|
|
omatches = omatches.filter((m) => {
|
|
|
|
|
return valarray.includes(m[prop]);
|
|
|
|
|
});
|
2025-09-01 16:26:05 +02:00
|
|
|
}
|
2025-11-03 09:17:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (omatches.length > 0) {
|
|
|
|
|
if (keys.length == 1) {
|
|
|
|
|
return omatches.map((oin, i) => {
|
|
|
|
|
return { value: oin, path: [{ object: oin, path: key + (omatches.length > 1 ? `[${i}]` : "") }, ...path] };
|
|
|
|
|
});
|
|
|
|
|
} else if (omatches.length == 1) {
|
2025-11-03 15:27:42 +01:00
|
|
|
return getValues(keys.slice(1), [{ object: omatches[0], path: key }, ...path]);
|
2025-09-01 16:26:05 +02:00
|
|
|
} else {
|
2025-11-03 09:17:09 +01:00
|
|
|
return omatches.reduce((res: FlagMatchRuleValue[] | undefined, oin: any, i: number) => {
|
|
|
|
|
let matches = getValues(keys.slice(1), [{ object: oin, path: key + "[" + i + "]" }, ...path]);
|
|
|
|
|
if (matches) {
|
|
|
|
|
const r2 = res || [];
|
|
|
|
|
r2.push(...matches);
|
|
|
|
|
return r2;
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}, undefined);
|
2025-09-01 16:26:05 +02:00
|
|
|
}
|
2025-11-03 09:17:09 +01:00
|
|
|
} else {
|
|
|
|
|
return undefined;
|
2025-09-01 16:26:05 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let result: FlagMatchRuleValue[] = [];
|
|
|
|
|
try {
|
2025-09-23 12:52:52 +02:00
|
|
|
let keys = flagPath.split("/");
|
|
|
|
|
result = getValues(keys, path) ?? [];
|
2025-09-01 16:26:05 +02:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Invalid RE", e);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
|
|
|
|
for (let idx = 0; idx < flagValues.length; idx++) {
|
|
|
|
|
const result = extractFlagValues(flagValues[idx].path, path);
|
2025-09-23 12:52:52 +02:00
|
|
|
if (result.length > 0) {
|
2025-11-12 15:20:31 +01:00
|
|
|
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) {}
|
2025-10-22 15:03:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-23 12:52:52 +02:00
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
const getFirstWithDataAsDate = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): Date | undefined => {
|
|
|
|
|
let val = getFirstWithData(flagValues, path);
|
2025-09-23 12:52:52 +02:00
|
|
|
if (val) {
|
|
|
|
|
try {
|
2025-10-22 15:03:56 +02:00
|
|
|
let date = new Date(Date.parse(val));
|
|
|
|
|
if (isNaN(date.valueOf())) {
|
|
|
|
|
// Try EXIF format
|
|
|
|
|
date = utils.parseExifDate(val);
|
|
|
|
|
}
|
2025-09-23 12:52:52 +02:00
|
|
|
return date;
|
2025-11-03 15:27:42 +01:00
|
|
|
} catch (error) {}
|
2025-09-23 12:52:52 +02:00
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
const getMultiple = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): (string | undefined)[] => {
|
|
|
|
|
let results: (string | undefined)[] = new Array(flagValues.length);
|
|
|
|
|
for (let idx = 0; idx < flagValues.length; idx++) {
|
|
|
|
|
const result = extractFlagValues(flagValues[idx].path, path);
|
|
|
|
|
if (result.length > 0) {
|
|
|
|
|
results[idx] = result[0].value as string;
|
|
|
|
|
} else {
|
|
|
|
|
results[idx] = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
export const mediaMetadataToMediaInterventionFlags = (mediaMetadata: MediaMetadata): MediaInterventionFlags => {
|
|
|
|
|
return {
|
|
|
|
|
creationDate: mediaMetadata.creationDate,
|
|
|
|
|
generator: mediaMetadata.generator,
|
|
|
|
|
modified: mediaMetadata.edits && mediaMetadata.edits.length > 0,
|
|
|
|
|
containsC2PA: mediaMetadata.containsC2PA,
|
2025-11-03 15:27:42 +01:00
|
|
|
containsEXIF: mediaMetadata.containsEXIF,
|
|
|
|
|
};
|
|
|
|
|
};
|
2025-10-23 10:13:14 +02:00
|
|
|
|
|
|
|
|
export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined => {
|
2025-08-28 10:27:57 +02:00
|
|
|
if (!proof) return undefined;
|
|
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
let edits: MediaMetadataEdit[] | undefined = undefined;
|
2025-08-28 10:27:57 +02:00
|
|
|
let valid = false;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let results = proof.integrity?.c2pa?.manifest_info.validation_results?.activeManifest;
|
|
|
|
|
if (results) {
|
|
|
|
|
valid = results.failure.length == 0 && results.success.length > 0;
|
|
|
|
|
}
|
2025-09-05 11:16:50 +02:00
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
const rootMatchPath = [{ object: proof, path: "" }];
|
2025-09-05 11:16:50 +02:00
|
|
|
|
2025-09-23 12:52:52 +02:00
|
|
|
let source: string | undefined = undefined;
|
|
|
|
|
let dateCreated: Date | undefined = undefined;
|
2025-10-23 10:13:14 +02:00
|
|
|
let dateCreatedSource: MediaMetadataPropertySource | undefined = undefined;
|
2025-09-23 12:52:52 +02:00
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
source = getFirstWithData(pathsC2PASource(), rootMatchPath);
|
2025-10-23 07:45:39 +02:00
|
|
|
if (!source) {
|
|
|
|
|
source = getFirstWithData(pathsExifSource(), rootMatchPath);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 15:03:56 +02:00
|
|
|
dateCreated = getFirstWithDataAsDate(pathsC2PACreationDate(), rootMatchPath);
|
|
|
|
|
if (dateCreated) {
|
2025-09-23 17:17:26 +02:00
|
|
|
dateCreatedSource = "c2pa";
|
2025-10-22 15:03:56 +02:00
|
|
|
} else {
|
|
|
|
|
dateCreated = getFirstWithDataAsDate(pathsExifCreationDate(), rootMatchPath);
|
|
|
|
|
if (dateCreated) {
|
|
|
|
|
dateCreatedSource = "exif";
|
|
|
|
|
}
|
2025-08-28 10:27:57 +02:00
|
|
|
}
|
2025-09-05 11:16:50 +02:00
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
let generator: MediaMetadataGenerator = "unknown";
|
|
|
|
|
let generatorSource: MediaMetadataPropertySource | undefined = undefined;
|
2025-09-09 10:56:15 +02:00
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
let digitalSourceType = extractFlagValues(
|
|
|
|
|
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/digitalSourceType",
|
|
|
|
|
rootMatchPath
|
|
|
|
|
)?.at(0)?.value as string;
|
2025-10-22 15:27:43 +02:00
|
|
|
if ([C2PASourceTypeScreenCapture].includes(digitalSourceType)) {
|
|
|
|
|
generator = "screenshot";
|
2025-09-09 10:56:15 +02:00
|
|
|
generatorSource = "c2pa";
|
2025-11-03 15:27:42 +01:00
|
|
|
} else if (
|
|
|
|
|
[C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture].includes(
|
|
|
|
|
digitalSourceType
|
|
|
|
|
)
|
|
|
|
|
) {
|
2025-10-22 15:27:43 +02:00
|
|
|
generator = "camera";
|
|
|
|
|
generatorSource = "c2pa";
|
2025-11-03 15:27:42 +01:00
|
|
|
} else if (
|
|
|
|
|
[C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia].includes(
|
|
|
|
|
digitalSourceType
|
|
|
|
|
)
|
|
|
|
|
) {
|
2025-10-22 15:27:43 +02:00
|
|
|
generator = "ai";
|
|
|
|
|
generatorSource = "c2pa";
|
2025-10-23 07:45:39 +02:00
|
|
|
} else if (getFirstWithData(pathsC2PACamera(), rootMatchPath)) {
|
|
|
|
|
generator = "camera";
|
|
|
|
|
generatorSource = "c2pa";
|
|
|
|
|
} else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) {
|
|
|
|
|
generator = "camera";
|
|
|
|
|
generatorSource = "exif";
|
2025-11-12 15:20:31 +01:00
|
|
|
} else if (getFirstWithData(pathsExifScreenshot(), rootMatchPath)) {
|
|
|
|
|
generator = "screenshot";
|
|
|
|
|
generatorSource = "exif";
|
|
|
|
|
} else if (getFirstWithData(pathsMetaScreenshot(), rootMatchPath)) {
|
2025-10-22 15:27:43 +02:00
|
|
|
generator = "screenshot";
|
|
|
|
|
generatorSource = "metadata";
|
2025-11-12 15:20:31 +01:00
|
|
|
} else if (getFirstWithData(pathsMetaAI(), rootMatchPath)) {
|
2025-10-22 15:27:43 +02:00
|
|
|
generator = "ai";
|
|
|
|
|
generatorSource = "metadata";
|
2025-09-09 10:56:15 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-23 12:52:52 +02:00
|
|
|
console.error("PROOF", proof);
|
|
|
|
|
|
|
|
|
|
const c2paEdits = extractFlagValues(
|
2025-10-22 15:27:43 +02:00
|
|
|
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[!!action=c2pa.created]",
|
2025-09-23 12:52:52 +02:00
|
|
|
rootMatchPath
|
|
|
|
|
);
|
|
|
|
|
if (c2paEdits.length > 0) {
|
|
|
|
|
edits = c2paEdits.map((edit) => {
|
|
|
|
|
return {
|
2025-11-03 15:27:42 +01:00
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
};
|
2025-09-23 12:52:52 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 15:27:42 +01:00
|
|
|
// Location
|
|
|
|
|
let location: MediaMetadataLocation | undefined = undefined;
|
|
|
|
|
let locationSource: MediaMetadataPropertySource = "c2pa"
|
|
|
|
|
let [lat, lon, latref, lonref] = getMultiple(pathsC2PALocation(), rootMatchPath);
|
|
|
|
|
if (!lat || !lon) {
|
|
|
|
|
[lat, lon, latref, lonref] = getMultiple(pathsExifLocation(), rootMatchPath);
|
|
|
|
|
locationSource = "exif"
|
|
|
|
|
}
|
|
|
|
|
if (lat && lon) {
|
|
|
|
|
try {
|
|
|
|
|
const latitude = toDegrees(getExifValue(lat), getExifValue(latref ?? ""));
|
|
|
|
|
const longitude = toDegrees(getExifValue(lon), getExifValue(lonref ?? ""));
|
|
|
|
|
if (latitude && longitude && latitude.length > 0 && longitude.length > 0) {
|
|
|
|
|
location = {
|
|
|
|
|
latitude,
|
|
|
|
|
longitude,
|
|
|
|
|
source: "exif",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 11:34:48 +02:00
|
|
|
// Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
|
2025-09-23 16:10:28 +02:00
|
|
|
if (source === undefined && dateCreated === undefined && generator === "unknown" && (!edits || edits.length == 0)) {
|
2025-09-09 11:34:48 +02:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 10:13:14 +02:00
|
|
|
const flags: MediaMetadata = {
|
2025-09-23 12:52:52 +02:00
|
|
|
device: source,
|
|
|
|
|
creationDate: dateCreated,
|
2025-09-23 17:17:26 +02:00
|
|
|
creationDateSource: dateCreatedSource,
|
2025-09-09 10:56:15 +02:00
|
|
|
generator: generator,
|
|
|
|
|
generatorSource: generatorSource,
|
|
|
|
|
edits: edits,
|
2025-09-23 16:10:28 +02:00
|
|
|
containsC2PA: proof.integrity?.c2pa !== undefined,
|
2025-11-03 15:27:42 +01:00
|
|
|
containsEXIF: proof.integrity?.exif !== undefined,
|
|
|
|
|
location: location,
|
2025-09-05 11:16:50 +02:00
|
|
|
};
|
|
|
|
|
return flags;
|
2025-08-28 10:27:57 +02:00
|
|
|
} catch (error) {}
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|