Pick up location from C2PA or Exif

This commit is contained in:
N-Pex 2025-11-03 15:27:42 +01:00
parent d6c8146096
commit 64b14cef40
2 changed files with 197 additions and 113 deletions

View file

@ -88,43 +88,18 @@ const updateDetails = () => {
}); });
} }
const exif = props.proof?.integrity?.exif; if (props.metadata?.location) {
if (exif) { const lat = props.metadata.location.latitude;
const getSimpleValue = (key: string): string | undefined => { const lon = props.metadata.location.longitude;
return exif ? (exif[key] as string)?.replace(/^"(.+(?="$))"$/, "$1") : undefined; const location = lat + " " + lon;
}; const link = "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(lat) + "," + encodeURIComponent(lon);
d.push({
const toDegrees = (dms: string | undefined, direction: string) => { icon: "$vuetify.icons.ic_exif_location",
if (!dms || dms.length == 0) return undefined; title: t("file_mode.cc_location"),
var parts = dms.split(/deg|min|sec/); value: location,
var d = parts[0]; link: link,
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;
};
// Location
try {
const lat = toDegrees(getSimpleValue("GPSLatitude"), getSimpleValue("GPSLatitudeRef") ?? "");
const lon = toDegrees(getSimpleValue("GPSLongitude"), getSimpleValue("GPSLongitudeRef") ?? "");
if (lat && lon && lat.length > 0 && lon.length > 0) {
const location = lat + " " + lon;
const link =
"https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(lat) + "," + encodeURIComponent(lon);
d.push({
icon: "$vuetify.icons.ic_exif_location",
title: t("file_mode.cc_location"),
value: location,
link: link,
});
}
} catch (error) { }
} }
details.value = d; details.value = d;
}; };

View file

@ -62,13 +62,19 @@ export type Proof = {
ai?: { inferenceResult?: AIInferenceResult }; ai?: { inferenceResult?: AIInferenceResult };
}; };
export type MediaMetadataGenerator = "unknown" | "camera" | "screenshot" | "ai"; export type MediaMetadataGenerator = "unknown" | "camera" | "screenshot" | "ai";
export type MediaMetadataPropertySource = "c2pa" | "exif" | "metadata"; export type MediaMetadataPropertySource = "c2pa" | "exif" | "metadata";
export type MediaMetadataEdit = { export type MediaMetadataEdit = {
editor: string; editor: string;
date?: Date; date?: Date;
} };
export type MediaMetadataLocation = {
latitude: string;
longitude: string;
source: MediaMetadataPropertySource;
};
export type MediaInterventionFlags = { export type MediaInterventionFlags = {
creationDate?: Date; creationDate?: Date;
@ -76,7 +82,7 @@ export type MediaInterventionFlags = {
modified?: boolean; modified?: boolean;
containsC2PA?: boolean; containsC2PA?: boolean;
containsEXIF?: boolean; containsEXIF?: boolean;
} };
export type MediaMetadata = { export type MediaMetadata = {
device?: string; device?: string;
@ -87,6 +93,7 @@ export type MediaMetadata = {
edits?: MediaMetadataEdit[]; edits?: MediaMetadataEdit[];
containsC2PA?: boolean; containsC2PA?: boolean;
containsEXIF?: boolean; containsEXIF?: boolean;
location?: MediaMetadataLocation;
}; };
type FlagMatchRule = { type FlagMatchRule = {
@ -98,7 +105,7 @@ type FlagMatchRule = {
type FlagMatchRulePathSegment = { type FlagMatchRulePathSegment = {
object: any; object: any;
path: string; path: string;
} };
type FlagMatchRuleValue = { type FlagMatchRuleValue = {
path: FlagMatchRulePathSegment[]; path: FlagMatchRulePathSegment[];
@ -114,51 +121,92 @@ type FlagMatchInfo = {
type FlagValue = { type FlagValue = {
path: string; path: string;
transform?: (value: any, match: FlagMatchRuleValue) => any; transform?: (value: any, match: FlagMatchRuleValue) => any;
} };
const pathsC2PASource = (): FlagValue[] => { const pathsC2PASource = (): FlagValue[] => {
return [ return [
{path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Make", transform: getExifMakeModelPrefixed }, {
{path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Model", transform: getExifMakeModelPrefixed }, path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Make",
{path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions|c2pa.actions.v2]/data/actions[action=c2pa.created]/softwareAgent/name"}, transform: getExifMakeModelPrefixed,
{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"} {
path: "integrity/c2pa/manifest_info/manifests[]/assertions[label=stds.exif]/data/exif:Model",
transform: getExifMakeModelPrefixed,
},
{
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 pathsExifSource = (): FlagValue[] => { const pathsExifSource = (): FlagValue[] => {
return [ return [
{path: "integrity/exif/Make", transform: getExifMakeModelNoPrefix }, { path: "integrity/exif/Make", transform: getExifMakeModelNoPrefix },
{path: "integrity/exif/Model", transform: getExifMakeModelNoPrefix } { path: "integrity/exif/Model", transform: getExifMakeModelNoPrefix },
]; ];
}; };
const pathsC2PACreationDate = (): FlagValue[] => { const pathsC2PACreationDate = (): FlagValue[] => {
return [ 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]/when",
{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) => { {
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 // Check if we have a timestamp offset
let offset = extractFlagValues("../exif:OffsetTimeOriginal", match.path)?.at(0)?.value; let offset = extractFlagValues("../exif:OffsetTimeOriginal", match.path)?.at(0)?.value;
return dayjs(Date.parse(value + (offset ? offset : ""))).format("lll"); return dayjs(Date.parse(value + (offset ? offset : ""))).format("lll");
}} },
},
]; ];
} };
const pathsC2PACamera = (): FlagValue[] => { const pathsC2PACamera = (): FlagValue[] => {
return [ 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:LensMake"}, 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:LensModel"}, },
{ 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" },
]; ];
}; };
const pathsExifCamera = (): FlagValue[] => { const pathsExifCamera = (): FlagValue[] => {
return [ return [
{path: "integrity/exif{SceneType=1|directly photographed image}"}, { path: "integrity/exif{SceneType=1|directly photographed image}" },
{path: "integrity/exif/LensMake"}, { path: "integrity/exif/LensMake" },
{path: "integrity/exif/LensModel"}, { path: "integrity/exif/LensModel" },
];
};
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" },
]; ];
}; };
@ -166,47 +214,62 @@ const getExifValue = (val: string): string => {
return val.replace(/^"(.+(?="$))"$/, "$1"); return val.replace(/^"(.+(?="$))"$/, "$1");
}; };
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;
};
const pathsExifCreationDate = (): FlagValue[] => { const pathsExifCreationDate = (): FlagValue[] => {
return [ return [
{path: "integrity/exif/DateTimeOriginal", transform: (value, match) => { {
path: "integrity/exif/DateTimeOriginal",
transform: (value, match) => {
let dateTimeOriginal = getExifValue(value); let dateTimeOriginal = getExifValue(value);
// Check if we have a timestamp offset // Check if we have a timestamp offset
let offset = extractFlagValues("../OffsetTimeOriginal", match.path)?.at(0)?.value as string; let offset = extractFlagValues("../OffsetTimeOriginal", match.path)?.at(0)?.value as string;
return dayjs(Date.parse(dateTimeOriginal + (offset ? getExifValue(offset) : ""))).format("lll"); return dayjs(Date.parse(dateTimeOriginal + (offset ? getExifValue(offset) : ""))).format("lll");
}} },
},
]; ];
} };
const getExifMakeModelNoPrefix = (ignoredvalue: any, match: FlagMatchRuleValue): string => { const getExifMakeModelNoPrefix = (ignoredvalue: any, match: FlagMatchRuleValue): string => {
return getExifMakeModel(match, ""); return getExifMakeModel(match, "");
} };
const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue): string => { const getExifMakeModelPrefixed = (ignoredvalue: any, match: FlagMatchRuleValue): string => {
return getExifMakeModel(match, "exif:"); return getExifMakeModel(match, "exif:");
} };
const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => { const getExifMakeModel = (match: FlagMatchRuleValue, prefix: string): string => {
// Make and model // Make and model
let makeAndModel = ""; let makeAndModel = "";
const make = extractFlagValues(`../${prefix}Make`, match.path)?.at(0)?.value as string; const make = extractFlagValues(`../${prefix}Make`, match.path)?.at(0)?.value as string;
const model = extractFlagValues(`../${prefix}Model`, match.path)?.at(0)?.value as string; const model = extractFlagValues(`../${prefix}Model`, match.path)?.at(0)?.value as string;
if (make) { if (make) {
makeAndModel += getExifValue(make); makeAndModel += getExifValue(make);
}
if (model) {
if (makeAndModel.length > 0) {
makeAndModel += ", ";
} }
if (model) { makeAndModel += getExifValue(model);
if (makeAndModel.length > 0) { }
makeAndModel += ", "; return makeAndModel;
} };
makeAndModel += getExifValue(model);
}
return makeAndModel;
}
const ruleScreenshotMeta = (): FlagMatchRule[] => { const ruleScreenshotMeta = (): FlagMatchRule[] => {
return [ return [
{ {
field: field: "name",
"name",
match: ["screenshot"], match: ["screenshot"],
description: "Screen capture", description: "Screen capture",
}, },
@ -260,10 +323,7 @@ const matchFlag = (rules: FlagMatchRule[], path: FlagMatchRulePathSegment[]) =>
}; };
const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => { const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
const getValues = ( const getValues = (keys: string[], path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] | undefined => {
keys: string[],
path: FlagMatchRulePathSegment[]
): FlagMatchRuleValue[] | undefined => {
if (keys.length == 0 || path.length == 0) return undefined; if (keys.length == 0 || path.length == 0) return undefined;
const o = path[0].object; const o = path[0].object;
@ -289,7 +349,13 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
} }
let nextObject = o[key]; let nextObject = o[key];
let omatches: any[] = nextObject ? (Array.isArray(nextObject) ? nextObject : hasBracket ? Object.values(nextObject) : [nextObject]) : []; let omatches: any[] = nextObject
? Array.isArray(nextObject)
? nextObject
: hasBracket
? Object.values(nextObject)
: [nextObject]
: [];
// Any constraints controlling what array object(s) to consider? // Any constraints controlling what array object(s) to consider?
if (optionalConstraint) { if (optionalConstraint) {
@ -321,7 +387,7 @@ const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]):
return { value: oin, path: [{ object: oin, path: key + (omatches.length > 1 ? `[${i}]` : "") }, ...path] }; return { value: oin, path: [{ object: oin, path: key + (omatches.length > 1 ? `[${i}]` : "") }, ...path] };
}); });
} else if (omatches.length == 1) { } else if (omatches.length == 1) {
return getValues(keys.slice(1), [{object: omatches[0], path: key}, ...path]); return getValues(keys.slice(1), [{ object: omatches[0], path: key }, ...path]);
} else { } else {
return omatches.reduce((res: FlagMatchRuleValue[] | undefined, oin: any, i: number) => { return omatches.reduce((res: FlagMatchRuleValue[] | undefined, oin: any, i: number) => {
let matches = getValues(keys.slice(1), [{ object: oin, path: key + "[" + i + "]" }, ...path]); let matches = getValues(keys.slice(1), [{ object: oin, path: key + "[" + i + "]" }, ...path]);
@ -354,20 +420,19 @@ const getFirstWithData = (flagValues: FlagValue[], path: FlagMatchRulePathSegmen
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] first = result[0];
transform = flagValues[idx].transform; transform = flagValues[idx].transform;
break; break;
} }
} }
if (first && first.value as string) { if (first && (first.value as string)) {
try { try {
let val = first.value as string; let val = first.value as string;
if (val && transform) { if (val && transform) {
val = transform(val, first); val = transform(val, first);
} }
return val; return val;
} catch (error) { } catch (error) {}
}
} }
return undefined; return undefined;
}; };
@ -382,21 +447,33 @@ const getFirstWithDataAsDate = (flagValues: FlagValue[], path: FlagMatchRulePath
date = utils.parseExifDate(val); date = utils.parseExifDate(val);
} }
return date; return date;
} catch (error) { } catch (error) {}
}
} }
return undefined; return undefined;
}; };
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;
};
export const mediaMetadataToMediaInterventionFlags = (mediaMetadata: MediaMetadata): MediaInterventionFlags => { export const mediaMetadataToMediaInterventionFlags = (mediaMetadata: MediaMetadata): MediaInterventionFlags => {
return { return {
creationDate: mediaMetadata.creationDate, creationDate: mediaMetadata.creationDate,
generator: mediaMetadata.generator, generator: mediaMetadata.generator,
modified: mediaMetadata.edits && mediaMetadata.edits.length > 0, modified: mediaMetadata.edits && mediaMetadata.edits.length > 0,
containsC2PA: mediaMetadata.containsC2PA, containsC2PA: mediaMetadata.containsC2PA,
containsEXIF: mediaMetadata.containsEXIF containsEXIF: mediaMetadata.containsEXIF,
} };
} };
export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined => { export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined => {
if (!proof) return undefined; if (!proof) return undefined;
@ -410,7 +487,7 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
valid = results.failure.length == 0 && results.success.length > 0; valid = results.failure.length == 0 && results.success.length > 0;
} }
const rootMatchPath = [{object: proof, path: ""}]; const rootMatchPath = [{ object: proof, path: "" }];
let source: string | undefined = undefined; let source: string | undefined = undefined;
let dateCreated: Date | undefined = undefined; let dateCreated: Date | undefined = undefined;
@ -431,18 +508,28 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
} }
} }
let generator: MediaMetadataGenerator = "unknown"; let generator: MediaMetadataGenerator = "unknown";
let generatorSource: MediaMetadataPropertySource | undefined = undefined; let generatorSource: MediaMetadataPropertySource | undefined = undefined;
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; 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;
if ([C2PASourceTypeScreenCapture].includes(digitalSourceType)) { if ([C2PASourceTypeScreenCapture].includes(digitalSourceType)) {
generator = "screenshot"; generator = "screenshot";
generatorSource = "c2pa"; generatorSource = "c2pa";
} else if ([C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture].includes(digitalSourceType)) { } else if (
[C2PASourceTypeDigitalCapture, C2PASourceTypeComputationalCapture, C2PASourceTypeCompositeCapture].includes(
digitalSourceType
)
) {
generator = "camera"; generator = "camera";
generatorSource = "c2pa"; generatorSource = "c2pa";
} else if ([C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia].includes(digitalSourceType)) { } else if (
[C2PASourceTypeTrainedAlgorithmicMedia, C2PASourceTypeCompositeWithTrainedAlgorithmicMedia].includes(
digitalSourceType
)
) {
generator = "ai"; generator = "ai";
generatorSource = "c2pa"; generatorSource = "c2pa";
} else if (getFirstWithData(pathsC2PACamera(), rootMatchPath)) { } else if (getFirstWithData(pathsC2PACamera(), rootMatchPath)) {
@ -468,20 +555,41 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
if (c2paEdits.length > 0) { if (c2paEdits.length > 0) {
edits = c2paEdits.map((edit) => { edits = c2paEdits.map((edit) => {
return { return {
editor: getFirstWithData([ editor:
{path: "softwareAgent/name"}, getFirstWithData(
{path: "softwareAgent"}, [{ path: "softwareAgent/name" }, { path: "softwareAgent" }, { path: "../../../claim_generator" }],
{path: "../../../claim_generator"} edit.path
], edit.path) ?? "", ) ?? "",
date: getFirstWithDataAsDate([ date: getFirstWithDataAsDate(
{path: "when"}, [{ path: "when" }, { path: "../../metadata/dateTime" }, { path: "../../../signature_info/time" }],
{path: "../../metadata/dateTime"}, edit.path
{path: "../../../signature_info/time"} ),
], edit.path) };
}
}); });
} }
// 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) {}
}
// Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults. // Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
if (source === undefined && dateCreated === undefined && generator === "unknown" && (!edits || edits.length == 0)) { if (source === undefined && dateCreated === undefined && generator === "unknown" && (!edits || edits.length == 0)) {
return undefined; return undefined;
@ -495,7 +603,8 @@ export const extractMediaMetadata = (proof?: Proof): MediaMetadata | undefined =
generatorSource: generatorSource, generatorSource: generatorSource,
edits: edits, edits: edits,
containsC2PA: proof.integrity?.c2pa !== undefined, containsC2PA: proof.integrity?.c2pa !== undefined,
containsEXIF: proof.integrity?.exif !== undefined containsEXIF: proof.integrity?.exif !== undefined,
location: location,
}; };
return flags; return flags;
} catch (error) {} } catch (error) {}