Move exif info into common "details" view
This commit is contained in:
parent
609e4a97c2
commit
0f7b9ac7ab
12 changed files with 308 additions and 230 deletions
|
|
@ -516,7 +516,14 @@ $hiliteColor: #4642f1;
|
|||
.attachment-info {
|
||||
text-align: start;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.attachment-info__content {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.attachment-info__quality {
|
||||
.attachment-info__quality__title {
|
||||
font-family: "Inter", sans-serif;
|
||||
|
|
@ -613,12 +620,13 @@ $hiliteColor: #4642f1;
|
|||
font-size: 14px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
margin-top: 8px;
|
||||
margin-top: 22px;
|
||||
padding-bottom: 4px;
|
||||
a, a:visited {
|
||||
font-weight: 700;
|
||||
color: #8A87FF;
|
||||
text-decoration: none !important;
|
||||
color: white;
|
||||
text-decoration: underline !important;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -633,11 +641,13 @@ $hiliteColor: #4642f1;
|
|||
display: flex;
|
||||
|
||||
.v-icon {
|
||||
color: #dad9fc;
|
||||
padding: 9.33px;
|
||||
margin-right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: black;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-row__text {
|
||||
|
|
@ -656,4 +666,17 @@ $hiliteColor: #4642f1;
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-info__download-button {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14 * $chat-text-size;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
border: 1px solid white;
|
||||
border-radius: 24 * $chat-text-size;
|
||||
height: 48 * $chat-text-size;
|
||||
|
||||
flex: 0 0 (48 * $chat-text-size);
|
||||
}
|
||||
}
|
||||
11
src/assets/icons/ic_copy.vue
Normal file
11
src/assets/icons/ic_copy.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5 2H9.73333C11.2268 2 11.9735 2 12.544 2.29065C13.0457 2.54631 13.4537 2.95426 13.7094 3.45603C14 4.02646 14 4.77319 14 6.26667V11M4.13333 14H9.53333C10.2801 14 10.6534 14 10.9387 13.8547C11.1895 13.7268 11.3935 13.5229 11.5213 13.272C11.6667 12.9868 11.6667 12.6134 11.6667 11.8667V6.46667C11.6667 5.71993 11.6667 5.34656 11.5213 5.06135C11.3935 4.81046 11.1895 4.60649 10.9387 4.47866C10.6534 4.33333 10.2801 4.33333 9.53333 4.33333H4.13333C3.3866 4.33333 3.01323 4.33333 2.72801 4.47866C2.47713 4.60649 2.27316 4.81046 2.14532 5.06135C2 5.34656 2 5.71993 2 6.46667V11.8667C2 12.6134 2 12.9868 2.14532 13.272C2.27316 13.5229 2.47713 13.7268 2.72801 13.8547C3.01323 14 3.3866 14 4.13333 14Z"
|
||||
stroke="white"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -2,14 +2,14 @@
|
|||
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.1487 15.7001H0.822597C0.376044 15.7001 0 15.3241 0 14.8775C0 14.431 0.376044 14.0549 0.822597 14.0549H14.1487C14.5952 14.0549 14.9713 14.431 14.9713 14.8775C14.9713 15.3241 14.5952 15.7001 14.1487 15.7001Z"
|
||||
fill="#161616" />
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M7.4974 12.1509C7.05085 12.1509 6.6748 11.7749 6.6748 11.3283V0.822597C6.6748 0.376044 7.05085 0 7.4974 0C7.94395 0 8.32 0.376044 8.32 0.822597V11.3283C8.32 11.7749 7.94395 12.1509 7.4974 12.1509Z"
|
||||
fill="#161616" />
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M7.49734 12.151C7.28581 12.151 7.07429 12.0805 6.90977 11.916L3.05531 8.03806C2.72627 7.70902 2.72627 7.19196 3.05531 6.88643C3.38435 6.55739 3.90141 6.55739 4.20695 6.88643L8.0614 10.7409C8.39044 11.0699 8.39044 11.587 8.0614 11.8925C7.92039 12.0805 7.70886 12.151 7.49734 12.151Z"
|
||||
fill="#161616" />
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M7.49739 12.151C7.28586 12.151 7.07434 12.0805 6.90982 11.916C6.58078 11.587 6.58078 11.0699 6.90982 10.7644L10.7878 6.88643C11.1168 6.55739 11.6339 6.55739 11.9394 6.88643C12.2685 7.21547 12.2685 7.73253 11.9394 8.03806L8.08496 11.916C7.92044 12.0805 7.70891 12.151 7.49739 12.151Z"
|
||||
fill="#161616" />
|
||||
fill="currentColor" />
|
||||
</svg></template>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<style>
|
||||
<style scoped>
|
||||
path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -514,8 +514,10 @@
|
|||
"cc_location": "Location"
|
||||
},
|
||||
"cc": {
|
||||
"content_credentials": "Content Credentials",
|
||||
"content_credentials_info": "Source or history information is available for this media to be verified.",
|
||||
"metadata-stripped": "Image has been compressed and stripped of metadata.\n\nWe think this image has additional file information. If you want to learn more, ask the sender to share the original."
|
||||
"details": "Details",
|
||||
"cc_info": "More information is available. Upload the image to {link}",
|
||||
"cc_info_link": "Content Credentials Verify",
|
||||
"metadata-stripped": "The image has been compressed and its metadata removed. If you’d like more context, ask the sender for the original file.",
|
||||
"download_image": "Download image"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,27 @@
|
|||
<template>
|
||||
<div v-if="props.flags">
|
||||
<div class="cc-detail-info" v-if="infoText !== undefined">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
<div class="attachment-info__detail-box">
|
||||
<div class="detail-title">
|
||||
{{ t("cc.content_credentials") }}
|
||||
<v-icon>$vuetify.icons.ic_cr</v-icon>
|
||||
{{ t("cc.details") }}
|
||||
<v-icon v-if="hasC2PA">$vuetify.icons.ic_cr</v-icon>
|
||||
</div>
|
||||
|
||||
<CCProperty v-for="d in details" :icon="d.icon" :title="d.title" :value="d.value">
|
||||
<template v-if="d.link" v-slot:default>
|
||||
<a :href="d.link">{{ d.value }}</a>
|
||||
</template>
|
||||
</CCProperty>
|
||||
|
||||
<div class="detail-subtitle" v-if="hasC2PA">
|
||||
{{ t("cc.content_credentials_info") }}
|
||||
<i18n-t keypath="cc.cc_info" tag="span">
|
||||
<template v-slot:link>
|
||||
<a href="https://contentcredentials.org/verify" target="_blank">{{ $t("cc.cc_info_link") }}</a>
|
||||
<v-icon>$vuetify.icons.ic_copy</v-icon>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="cc-detail-info" v-if="infoText !== undefined">
|
||||
{{ infoText }}
|
||||
</div>
|
||||
|
||||
<CCProperty
|
||||
v-if="props.flags?.device"
|
||||
icon="$vuetify.icons.ic_media_camera"
|
||||
:title="t('file_mode.cc_source')"
|
||||
:value="props.flags?.device"
|
||||
/>
|
||||
<CCProperty
|
||||
v-if="creationDate"
|
||||
icon="$vuetify.icons.ic_exif_time"
|
||||
:title="t('file_mode.cc_capture_timestamp')"
|
||||
:value="creationDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -32,7 +30,7 @@ import { useI18n } from "vue-i18n";
|
|||
import { Proof, ProofHintFlags } from "../../models/proof";
|
||||
import { computed, ref, Ref, watch } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import CCProperty from "./CCProperty.vue";
|
||||
import CCProperty, { CCPropertyProps } from "./CCProperty.vue";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
|
@ -42,15 +40,109 @@ const { t } = useI18n();
|
|||
const props = defineProps<{
|
||||
proof?: Proof;
|
||||
flags?: ProofHintFlags;
|
||||
metaStripped?: boolean;
|
||||
}>();
|
||||
|
||||
const infoText: Ref<string | undefined> = ref(undefined);
|
||||
const creationDate: Ref<string | undefined> = ref(undefined);
|
||||
|
||||
const details: Ref<(CCPropertyProps & { link?: string })[]> = ref([]);
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return props.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
const updateDetails = () => {
|
||||
let d: (CCPropertyProps & { link?: string })[] = [];
|
||||
|
||||
if (props.flags?.device) {
|
||||
d.push({
|
||||
icon: "$vuetify.icons.ic_media_camera",
|
||||
title: t("file_mode.cc_source"),
|
||||
value: props.flags?.device,
|
||||
});
|
||||
}
|
||||
|
||||
if (creationDate.value) {
|
||||
d.push({
|
||||
icon: "$vuetify.icons.ic_exif_time",
|
||||
title: t("file_mode.cc_capture_timestamp"),
|
||||
value: creationDate.value,
|
||||
});
|
||||
}
|
||||
|
||||
const exif = props.proof?.integrity?.exif;
|
||||
if (exif) {
|
||||
const getSimpleValue = (key: string): string | undefined => {
|
||||
return exif ? (exif[key] as string)?.replace(/^"(.+(?="$))"$/, "$1") : undefined;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// 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
|
||||
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;
|
||||
};
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
|
|
@ -84,9 +176,15 @@ watch(
|
|||
result += t("file_mode.old_photo");
|
||||
}
|
||||
|
||||
if (props.metaStripped) {
|
||||
result += t("cc.metadata-stripped");
|
||||
}
|
||||
|
||||
infoText.value = result === "" ? undefined : result;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
updateDetails();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
export type CCPropertyProps = {
|
||||
icon: string;
|
||||
title: string;
|
||||
value?: string;
|
||||
}>();
|
||||
}
|
||||
const props = defineProps<CCPropertyProps>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -31,11 +31,15 @@ const infoText = computed(() => {
|
|||
return "<b>This image is generated by AI.</b> Take a closer look at the file details.";
|
||||
} else if (flags[0].generator === "screenshot") {
|
||||
return "<b>This is a screenshot.</b> Take a closer look at the file details.";
|
||||
} else if (flags[0].edits) {
|
||||
return "<b>This image has been modified.</b> Take a closer look at the file details.";
|
||||
}
|
||||
} else if (flags.some((f) => f.generator === "ai")) {
|
||||
return "<b>Contains AI generated media.</b> Take a closer look at the file details for each.";
|
||||
} else if (flags.some((f) => f.generator === "screenshot")) {
|
||||
return "<b>Contains screenshots.</b> Take a closer look at the file details for each.";
|
||||
} else if (flags.some((f) => f.edits?.length)) {
|
||||
return "<b>Contains modified media.</b> Take a closer look at the file details for each.";
|
||||
}
|
||||
return "TODO - Content Credentials Info";
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
<template>
|
||||
<div v-if="exif">
|
||||
<div class="detail-title">
|
||||
{{ t("file_mode.exif_data") }}
|
||||
</div>
|
||||
<CCProperty v-if="makeAndModel" icon="$vuetify.icons.ic_exif_device_camera" :title="t('file_mode.cc_source')" :value="makeAndModel" />
|
||||
<CCProperty v-if="dateTime" icon="$vuetify.icons.ic_exif_time" :title="t('file_mode.cc_capture_timestamp')" :value="dateTime" />
|
||||
<CCProperty v-if="location" icon="$vuetify.icons.ic_exif_location" :title="t('file_mode.cc_location')">
|
||||
<template v-slot:default>
|
||||
<a :href="locationLink">{{ location }}</a>
|
||||
</template>
|
||||
</CCProperty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import CCProperty from "./CCProperty.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
exif?: { [key: string]: string | Object };
|
||||
}>();
|
||||
const { exif } = props;
|
||||
|
||||
const getSimpleValue = (key: string): string | undefined => {
|
||||
return exif ? (exif[key] as string)?.replace(/^"(.+(?="$))"$/, "$1") : undefined;
|
||||
};
|
||||
|
||||
const toDegrees = (dms: string, direction: string) => {
|
||||
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 getLocation = () => {
|
||||
try {
|
||||
if (exif) {
|
||||
const gpsLat = getSimpleValue("GPSLatitude");
|
||||
const gpsLon = getSimpleValue("GPSLongitude");
|
||||
if (gpsLat && gpsLon) {
|
||||
const lat = toDegrees(gpsLat, getSimpleValue("GPSLatitudeRef") ?? "");
|
||||
const lon = toDegrees(gpsLon, getSimpleValue("GPSLongitudeRef") ?? "");
|
||||
return {lat, lon};
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const location = computed(() => {
|
||||
const pos = getLocation();
|
||||
if (pos) {
|
||||
return pos.lat + " " + pos.lon;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const locationLink = computed(() => {
|
||||
const pos = getLocation();
|
||||
if (pos) {
|
||||
return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(pos.lat) + "," + encodeURIComponent(pos.lon);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
|
||||
});
|
||||
|
||||
const dateTime = computed(() => {
|
||||
try {
|
||||
if (exif) {
|
||||
const date = getSimpleValue("DateTimeOriginal");
|
||||
const dateOffset = getSimpleValue("OffsetTimeOriginal");
|
||||
if (date) {
|
||||
return dayjs(Date.parse(date + (dateOffset ? dateOffset : ""))).format("lll");
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const makeAndModel = computed(() => {
|
||||
let result = "";
|
||||
if (exif) {
|
||||
const make = getSimpleValue("Make");
|
||||
const model = getSimpleValue("Model");
|
||||
if (make) {
|
||||
result += make;
|
||||
}
|
||||
if (model) {
|
||||
if (result.length > 0) {
|
||||
result += ", ";
|
||||
}
|
||||
result += model;
|
||||
}
|
||||
}
|
||||
return result.length > 0 ? result : undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
|
|
@ -28,8 +28,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<C2PAInfo class="attachment-info__detail-box" v-if="showC2PAInfo" :proof="attachment.proof" :flags="attachment.proofHintFlags" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment.proof?.integrity?.exif" />
|
||||
<C2PAInfo class="attachment-info__detail-box" v-if="showC2PAInfo || hasExif" :proof="attachment.proof" :flags="attachment.proofHintFlags" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -37,7 +36,6 @@
|
|||
import { Attachment } from "../../models/attachment";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import C2PAInfo from "../content-credentials/C2PAInfo.vue";
|
||||
import EXIFInfo from "../content-credentials/EXIFInfo.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
<template>
|
||||
<div class="attachment-info">
|
||||
<div v-if="loadingProof">
|
||||
<div style="font-size: 0.7em; opacity: 0.7">
|
||||
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
||||
<div class="attachment-info__content">
|
||||
<div v-if="loadingProof">
|
||||
<div style="font-size: 0.7em; opacity: 0.7">
|
||||
<v-progress-circular indeterminate class="mb-0"></v-progress-circular>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<C2PAInfo :proof="attachment?.proof" :flags="attachment?.proofHintFlags" :metaStripped="metaStripped" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="metaStripped" class="cc-detail-info white-space-pre">{{ t("cc.metadata-stripped") }}</div>
|
||||
<C2PAInfo class="attachment-info__detail-box" :proof="attachment?.proof" :flags="attachment?.proofHintFlags" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment?.proof?.integrity?.exif" />
|
||||
</div>
|
||||
<v-btn variant="flat" block class="attachment-info__download-button" @click.stop="() => {}">
|
||||
<v-icon color="white" class="mr-2">$vuetify.icons.ic_download</v-icon>{{ $t("cc.download_image") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import C2PAInfo from "../content-credentials/C2PAInfo.vue";
|
||||
import EXIFInfo from "../content-credentials/EXIFInfo.vue";
|
||||
import { computed, onMounted, Ref, ref } from "vue";
|
||||
import { onMounted, Ref, ref } from "vue";
|
||||
import { EventAttachment } from "../../models/eventAttachment";
|
||||
import proofmode from "../../plugins/proofmode";
|
||||
import { extractProofHintFlags } from "../../models/proof";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { attachment } = defineProps<{
|
||||
attachment?: EventAttachment;
|
||||
|
|
@ -41,7 +40,10 @@ onMounted(() => {
|
|||
if (data && data.data) {
|
||||
return proofmode.proofCheckSource(data.data).then((res) => {
|
||||
a.proof = res;
|
||||
a.proofHintFlags = extractProofHintFlags(a.proof);
|
||||
if (res?.integrity?.c2pa) {
|
||||
// If we have proof, overwrite the flags
|
||||
a.proofHintFlags = extractProofHintFlags(a.proof);
|
||||
}
|
||||
metaStripped.value = a?.proof?.integrity?.c2pa === undefined && a?.proof?.integrity?.exif === undefined;
|
||||
});
|
||||
}
|
||||
|
|
@ -51,17 +53,10 @@ onMounted(() => {
|
|||
loadingProof.value = false;
|
||||
});
|
||||
} else {
|
||||
metaStripped.value = attachment?.proof?.integrity?.c2pa === undefined && attachment?.proof?.integrity?.exif === undefined;
|
||||
metaStripped.value =
|
||||
attachment?.proof?.integrity?.c2pa === undefined && attachment?.proof?.integrity?.exif === undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return attachment?.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
const hasExif = computed(() => {
|
||||
return attachment?.proof?.integrity?.exif !== undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ export type Proof = {
|
|||
|
||||
export type ProofHintFlagsGenerator = "unknown" | "camera" | "screenshot" | "ai";
|
||||
export type ProofHintFlagsGeneratorSource = "c2pa" | "exif" | "metadata";
|
||||
export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
||||
//export type ProofHintFlagsEditor = "unknown" | "manual" | "ai";
|
||||
|
||||
export type ProofHintFlagsEdit = {
|
||||
editor: ProofHintFlagsEditor;
|
||||
editor: string;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
|
|
@ -82,9 +82,14 @@ type FlagMatchRule = {
|
|||
description: string;
|
||||
};
|
||||
|
||||
type FlagMatchRuleValue = {
|
||||
type FlagMatchRulePathSegment = {
|
||||
object: any;
|
||||
path: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type FlagMatchRuleValue = {
|
||||
path: FlagMatchRulePathSegment[];
|
||||
value: string | object;
|
||||
};
|
||||
|
||||
type FlagMatchInfo = {
|
||||
|
|
@ -161,18 +166,18 @@ const ruleAiMeta = (): FlagMatchRule[] => {
|
|||
];
|
||||
};
|
||||
|
||||
const matchFlag = (rules: FlagMatchRule[], file: any) => {
|
||||
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, file);
|
||||
const values = extractFlagValues(rule.field, path);
|
||||
values.forEach((v) => {
|
||||
re.forEach((r) => {
|
||||
if (r.test(v.value)) {
|
||||
if (typeof v.value === "string" && r.test(v.value)) {
|
||||
result = true;
|
||||
resultInfo.push({ field: v.path, value: v.value, re: r.source });
|
||||
resultInfo.push({ field: v.path.map((v) => v.path).join("/"), value: v.value, re: r.source });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -183,41 +188,53 @@ const matchFlag = (rules: FlagMatchRule[], file: any) => {
|
|||
return { result: result, matches: resultInfo };
|
||||
};
|
||||
|
||||
const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] => {
|
||||
const extractFlagValues = (flagPath: string, path: FlagMatchRulePathSegment[]): FlagMatchRuleValue[] => {
|
||||
const getValues = (
|
||||
path: string[],
|
||||
objectPath: any[],
|
||||
actualPath: string,
|
||||
o: any
|
||||
keys: string[],
|
||||
path: FlagMatchRulePathSegment[]
|
||||
): FlagMatchRuleValue[] | undefined => {
|
||||
if (path.length == 0 || o == undefined) return undefined;
|
||||
let part = path[0];
|
||||
const lastBracket = part.lastIndexOf("[");
|
||||
if (part === "..") {
|
||||
return getValues(path.slice(1), objectPath.slice(1), actualPath + "/..", objectPath[0]);
|
||||
if (keys.length == 0 || path.length == 0) return undefined;
|
||||
|
||||
const o = path[0].object;
|
||||
let key = keys[0];
|
||||
|
||||
const lastBracket = key.lastIndexOf("[");
|
||||
if (key === "..") {
|
||||
return getValues(keys.slice(1), path.slice(1));
|
||||
}
|
||||
if (part.endsWith("]") && lastBracket > 0) {
|
||||
const optionalConstraint = part.substring(lastBracket + 1, part.length - 1);
|
||||
part = part.substring(0, lastBracket);
|
||||
if (o[part] != undefined) {
|
||||
let opart: any[] = o[part];
|
||||
if (key.endsWith("]") && lastBracket > 0) {
|
||||
const optionalConstraint = key.substring(lastBracket + 1, key.length - 1);
|
||||
key = key.substring(0, lastBracket);
|
||||
if (o[key] != undefined) {
|
||||
let opart: any[] = o[key];
|
||||
if (!Array.isArray(opart)) {
|
||||
opart = Object.values(opart) ?? [];
|
||||
}
|
||||
|
||||
// Any constraints controlling what array object(s) to consider?
|
||||
if (optionalConstraint) {
|
||||
const [prop, val] = optionalConstraint.split("=");
|
||||
opart = opart.filter((item) => item[prop] === val);
|
||||
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("=");
|
||||
if (opart.some((item) => item[prop] === val)) {
|
||||
return undefined;
|
||||
}
|
||||
} else if (optionalConstraint.startsWith("!")) {
|
||||
const [prop, val] = optionalConstraint.substring(1).split("=");
|
||||
opart = opart.filter((item) => item[prop] !== val);
|
||||
} else {
|
||||
const [prop, val] = optionalConstraint.split("=");
|
||||
opart = opart.filter((item) => item[prop] === val);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.length == 1) {
|
||||
let strings = opart as string[];
|
||||
return strings.map((s, i) => ({ path: actualPath + "/" + part + "[" + i + "]", value: s }));
|
||||
if (keys.length == 1) {
|
||||
return opart.map((s, i) => {
|
||||
return { value: s, path: [{ object: o, path: key + "[" + i + "]"}, ... path] };
|
||||
});
|
||||
} else {
|
||||
return opart.reduce((res: FlagMatchRuleValue[] | undefined, o: any, i: number) => {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
let matches = getValues(path.slice(1), newObjectPaths, actualPath + "/" + part + "[" + i + "]", o);
|
||||
return opart.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);
|
||||
|
|
@ -230,12 +247,11 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
|
|||
return undefined;
|
||||
}
|
||||
} else {
|
||||
if (o[part] != undefined) {
|
||||
if (path.length == 1) {
|
||||
return [{ path: actualPath + "/" + part, value: o[part] }];
|
||||
if (o[key] != undefined) {
|
||||
if (keys.length == 1) {
|
||||
return [{ value: o[key], path: [{object: o[key], path: key}, ...path] }];
|
||||
} else {
|
||||
const newObjectPaths = [o, ...objectPath];
|
||||
return getValues(path.slice(1), newObjectPaths, actualPath + "/" + part, o[part]);
|
||||
return getValues(keys.slice(1), [{object: o[key], path: key}, ...path]);
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
|
|
@ -245,14 +261,43 @@ const extractFlagValues = (flagPath: string, file: any): FlagMatchRuleValue[] =>
|
|||
|
||||
let result: FlagMatchRuleValue[] = [];
|
||||
try {
|
||||
let parts = flagPath.split("/");
|
||||
result = getValues(parts, [], "", file) ?? [];
|
||||
let keys = flagPath.split("/");
|
||||
result = getValues(keys, path) ?? [];
|
||||
} catch (e) {
|
||||
console.error("Invalid RE", e);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const getFirstWithData = (flagPaths: string[], path: FlagMatchRulePathSegment[]): string | undefined => {
|
||||
for (let idx = 0; idx < flagPaths.length; idx++) {
|
||||
const result = extractFlagValues(flagPaths[idx], path);
|
||||
if (result.length > 0) {
|
||||
return result[0].value as string;
|
||||
}
|
||||
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getFirstWithDataAsDate = (flagPaths: string[], path: FlagMatchRulePathSegment[]): Date | undefined => {
|
||||
const val = getFirstWithData(flagPaths, path);
|
||||
if (val) {
|
||||
try {
|
||||
const date = new Date(Date.parse(val));
|
||||
return date;
|
||||
} catch (error) {}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const softwareAgentFromAction = (action: FlagMatchRuleValue): string | undefined => {
|
||||
const agent = getFirstWithData([
|
||||
"softwareAgent", "../../../claim_generator"
|
||||
], action.path);
|
||||
return agent;
|
||||
};
|
||||
|
||||
export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined => {
|
||||
if (!proof) return undefined;
|
||||
|
||||
|
|
@ -265,46 +310,59 @@ export const extractProofHintFlags = (proof?: Proof): ProofHintFlags | undefined
|
|||
valid = results.failure.length == 0 && results.success.length > 0;
|
||||
}
|
||||
|
||||
const source = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/softwareAgent",
|
||||
proof
|
||||
);
|
||||
const rootMatchPath = [{object: proof, path: ""}];
|
||||
|
||||
const dateCreated = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]/../../../../signature_info/time",
|
||||
proof
|
||||
let source: string | undefined = undefined;
|
||||
let dateCreated: Date | undefined = undefined;
|
||||
|
||||
const creationAction = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[action=c2pa.created]",
|
||||
rootMatchPath
|
||||
);
|
||||
let date: Date | undefined = undefined;
|
||||
if (dateCreated && dateCreated.length == 1) {
|
||||
try {
|
||||
date = new Date(Date.parse(dateCreated[0].value));
|
||||
} catch (error) {}
|
||||
if (creationAction.length > 0) {
|
||||
source = softwareAgentFromAction(creationAction[0]);
|
||||
dateCreated = getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], creationAction[0].path);
|
||||
}
|
||||
console.log("DATE CREATED", date);
|
||||
console.log("DATE CREATED", dateCreated);
|
||||
|
||||
let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), proof).result ? "ai" : matchFlag(ruleScreenshotC2PA(), proof).result ? "screenshot" : matchFlag(ruleCamera(), proof).result ? "camera" : "unknown";
|
||||
let generator: ProofHintFlagsGenerator = matchFlag(ruleAiGenerated(), rootMatchPath).result ? "ai" : matchFlag(ruleScreenshotC2PA(), rootMatchPath).result ? "screenshot" : matchFlag(ruleCamera(), rootMatchPath).result ? "camera" : "unknown";
|
||||
let generatorSource: ProofHintFlagsGeneratorSource | undefined = undefined;
|
||||
|
||||
if (generator !== "unknown" && valid) {
|
||||
generatorSource = "c2pa";
|
||||
} else {
|
||||
if (matchFlag(ruleScreenshotMeta(), proof).result) {
|
||||
if (matchFlag(ruleScreenshotMeta(), rootMatchPath).result) {
|
||||
generator = "screenshot";
|
||||
generatorSource = "metadata";
|
||||
} else if (matchFlag(ruleAiMeta(), proof).result) {
|
||||
} else if (matchFlag(ruleAiMeta(), rootMatchPath).result) {
|
||||
generator = "ai";
|
||||
generatorSource = "metadata";
|
||||
}
|
||||
}
|
||||
|
||||
console.error("PROOF", proof);
|
||||
|
||||
const c2paEdits = extractFlagValues(
|
||||
"integrity/c2pa/manifest_info/manifests[]/assertions[label=c2pa.actions]/data/actions[!!action=c2pa.created]",
|
||||
rootMatchPath
|
||||
);
|
||||
if (c2paEdits.length > 0) {
|
||||
edits = c2paEdits.map((edit) => {
|
||||
return {
|
||||
editor: softwareAgentFromAction(edit) ?? "",
|
||||
date: getFirstWithDataAsDate(["when", "../../metadata/dateTime", "../../../signature_info/time"], edit.path)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do we have any data? Else, return "undefined", we don't just want to send an object with all defaults.
|
||||
if (source.length === 0 && dateCreated.length === 0 && generator === "unknown") {
|
||||
if (source === undefined && dateCreated === undefined && generator === "unknown") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const flags: ProofHintFlags = {
|
||||
device: source && source.length == 1 ? source[0].value : undefined,
|
||||
creationDate: date,
|
||||
device: source,
|
||||
creationDate: dateCreated,
|
||||
generator: generator,
|
||||
generatorSource: generatorSource,
|
||||
edits: edits,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue