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 = {
|
||||
path: string;
|
||||
transform?: (value: any, match: FlagMatchRuleValue) => any;
|
||||
matches?: string[];
|
||||
description?: string; // Not currently used
|
||||
};
|
||||
|
||||
const pathsC2PASource = (): FlagValue[] => {
|
||||
|
|
@ -176,9 +178,7 @@ const pathsC2PACreationDate = (): FlagValue[] => {
|
|||
|
||||
const pathsC2PACamera = (): FlagValue[] => {
|
||||
return [
|
||||
{
|
||||
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:SceneType", matches: ["^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:LensModel" },
|
||||
];
|
||||
|
|
@ -186,12 +186,31 @@ const pathsC2PACamera = (): FlagValue[] => {
|
|||
|
||||
const pathsExifCamera = (): FlagValue[] => {
|
||||
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/LensModel" },
|
||||
];
|
||||
};
|
||||
|
||||
const pathsExifScreenshot = (): FlagValue[] => {
|
||||
return [
|
||||
{
|
||||
path: "integrity/exif/UserComment",
|
||||
transform: exifStringTransform,
|
||||
matches: ["^Screenshot$"],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const pathsMetaScreenshot = (): FlagValue[] => {
|
||||
return [
|
||||
{
|
||||
path: "name",
|
||||
matches: ["screenshot"],
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const pathsC2PALocation = (): FlagValue[] => {
|
||||
return [
|
||||
{ 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 => {
|
||||
return val.replace(/^"(.+(?="$))"$/, "$1");
|
||||
};
|
||||
|
|
@ -249,6 +292,34 @@ const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue):
|
|||
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 => {
|
||||
// Make and model
|
||||
let makeAndModel = "";
|
||||
|
|
@ -266,61 +337,6 @@ const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string =>
|
|||
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 getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => {
|
||||
|
|
@ -335,17 +351,12 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
|
|||
|
||||
const lastBracket = key.lastIndexOf("[");
|
||||
const hasBracket = key.endsWith("]") && lastBracket > 0;
|
||||
const lastBrace = key.lastIndexOf("{");
|
||||
const hasBrace = key.endsWith("}") && lastBrace > 0;
|
||||
|
||||
let optionalConstraint: String | undefined = undefined;
|
||||
|
||||
if (hasBracket) {
|
||||
optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
||||
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];
|
||||
|
|
@ -415,25 +426,30 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
|
|||
};
|
||||
|
||||
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) {
|
||||
first = result[0];
|
||||
transform = flagValues[idx].transform;
|
||||
break;
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
@ -538,10 +554,13 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
|
|||
} else if (getFirstWithData(pathsExifCamera(), rootMatchPath)) {
|
||||
generator = "camera";
|
||||
generatorSource = "exif";
|
||||
} else if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) {
|
||||
} else if (getFirstWithData(pathsExifScreenshot(), rootMatchPath)) {
|
||||
generator = "screenshot";
|
||||
generatorSource = "exif";
|
||||
} else if (getFirstWithData(pathsMetaScreenshot(), rootMatchPath)) {
|
||||
generator = "screenshot";
|
||||
generatorSource = "metadata";
|
||||
} else if (matchFlag(ruleAiMeta(), rootMatchPath).result) {
|
||||
} else if (getFirstWithData(pathsMetaAI(), rootMatchPath)) {
|
||||
generator = "ai";
|
||||
generatorSource = "metadata";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue