Cleanup metadata code and detect Mac screenshots via UserData Exif field.
This commit is contained in:
parent
c7e5375927
commit
f87446c6a4
1 changed files with 99 additions and 80 deletions
|
|
@ -121,6 +121,8 @@ type FlagMatchInfo = {
|
||||||
type FlagValue = {
|
type FlagValue = {
|
||||||
path: string;
|
path: string;
|
||||||
transform?: (value: any, match: FlagMatchRuleValue) => any;
|
transform?: (value: any, match: FlagMatchRuleValue) => any;
|
||||||
|
matches?: string[];
|
||||||
|
description?: string; // Not currently used
|
||||||
};
|
};
|
||||||
|
|
||||||
const pathsC2PASource = (): FlagValue[] => {
|
const pathsC2PASource = (): FlagValue[] => {
|
||||||
|
|
@ -176,9 +178,7 @@ const pathsC2PACreationDate = (): FlagValue[] => {
|
||||||
|
|
||||||
const pathsC2PACamera = (): FlagValue[] => {
|
const pathsC2PACamera = (): FlagValue[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{ 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:SceneType=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:LensMake" },
|
||||||
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensModel" },
|
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:LensModel" },
|
||||||
];
|
];
|
||||||
|
|
@ -186,12 +186,31 @@ const pathsC2PACamera = (): FlagValue[] => {
|
||||||
|
|
||||||
const pathsExifCamera = (): FlagValue[] => {
|
const pathsExifCamera = (): FlagValue[] => {
|
||||||
return [
|
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/LensMake" },
|
||||||
{ path: "integrity/exif/LensModel" },
|
{ 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[] => {
|
const pathsC2PALocation = (): FlagValue[] => {
|
||||||
return [
|
return [
|
||||||
{ path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/GPSLatitude" },
|
{ 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 => {
|
const getExifValue = (val: string): string => {
|
||||||
return val.replace(/^"(.+(?="$))"$/, "$1");
|
return val.replace(/^"(.+(?="$))"$/, "$1");
|
||||||
};
|
};
|
||||||
|
|
@ -249,6 +292,34 @@ const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue):
|
||||||
return getExifMakeModel(match, "exif:");
|
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 => {
|
const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => {
|
||||||
// Make and model
|
// Make and model
|
||||||
let makeAndModel = "";
|
let makeAndModel = "";
|
||||||
|
|
@ -266,61 +337,6 @@ const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string =>
|
||||||
return makeAndModel;
|
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 extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
|
||||||
const getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => {
|
const getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => {
|
||||||
|
|
@ -335,17 +351,12 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
|
||||||
|
|
||||||
const lastBracket = key.lastIndexOf("[");
|
const lastBracket = key.lastIndexOf("[");
|
||||||
const hasBracket = key.endsWith("]") && lastBracket > 0;
|
const hasBracket = key.endsWith("]") && lastBracket > 0;
|
||||||
const lastBrace = key.lastIndexOf("{");
|
|
||||||
const hasBrace = key.endsWith("}") && lastBrace > 0;
|
|
||||||
|
|
||||||
let optionalConstraint: String | undefined = undefined;
|
let optionalConstraint: String | undefined = undefined;
|
||||||
|
|
||||||
if (hasBracket) {
|
if (hasBracket) {
|
||||||
optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
||||||
key = key.substring(0, lastBracket);
|
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];
|
let nextObject = o[key];
|
||||||
|
|
@ -415,25 +426,30 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
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++) {
|
for (let idx = 0; idx < flagValues.length; idx++) {
|
||||||
const result = extractFlagValues(flagValues[idx].path, path);
|
const result = extractFlagValues(flagValues[idx].path, path);
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
first = result[0];
|
try {
|
||||||
transform = flagValues[idx].transform;
|
let first = result[0];
|
||||||
break;
|
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;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -538,10 +554,13 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
|
||||||
} else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) {
|
} else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) {
|
||||||
generator = "camera";
|
generator = "camera";
|
||||||
generatorSource = "exif";
|
generatorSource = "exif";
|
||||||
} else if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) {
|
} else if (getFirstWithData(pathsExifScreenshot(), rootMatchPath)) {
|
||||||
|
generator = "screenshot";
|
||||||
|
generatorSource = "exif";
|
||||||
|
} else if (getFirstWithData(pathsMetaScreenshot(), rootMatchPath)) {
|
||||||
generator = "screenshot";
|
generator = "screenshot";
|
||||||
generatorSource = "metadata";
|
generatorSource = "metadata";
|
||||||
} else if (matchFlag(ruleAiMeta(), rootMatchPath).result) {
|
} else if (getFirstWithData(pathsMetaAI(), rootMatchPath)) {
|
||||||
generator = "ai";
|
generator = "ai";
|
||||||
generatorSource = "metadata";
|
generatorSource = "metadata";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue