Start on C2PA and Exif info boxes on upload
This commit is contained in:
parent
114c09eb50
commit
54a1c05ddf
14 changed files with 337 additions and 49 deletions
|
|
@ -336,7 +336,6 @@ import emitter from 'tiny-emitter/instance';
|
|||
import { markRaw } from "vue";
|
||||
import timerIcon from '@/assets/icons/ic_timer.svg';
|
||||
import proofmode from "../plugins/proofmode.js";
|
||||
import C2PABadge from "./c2pa/C2PABadge.vue";
|
||||
import { consoleWarn } from "vuetify/lib/util/console.mjs";
|
||||
|
||||
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
|
||||
|
|
@ -392,7 +391,6 @@ export default {
|
|||
MessageOperationsChannel,
|
||||
RoomExport,
|
||||
EmojiPicker,
|
||||
C2PABadge
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
<template>
|
||||
<div v-if="hasC2PA" class="c2pa-badge">
|
||||
<v-icon>$vuetify.icons.ic_cr</v-icon>
|
||||
<span>This image contains C2PA data</span>
|
||||
</div>
|
||||
<div v-if="hasExif" class="c2pa-badge">
|
||||
<v-icon>camera_marker</v-icon>
|
||||
<span>This image contains EXIF data</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
proof?: {
|
||||
name?: string;
|
||||
json?: string;
|
||||
integrity?: { pgp?: any; c2pa?: any; exif?: any; opentimestamps?: any };
|
||||
};
|
||||
}>();
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return props.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
const hasExif = computed(() => {
|
||||
return props.proof?.integrity?.exif !== undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
73
src/components/content-credentials/C2PAInfo.vue
Normal file
73
src/components/content-credentials/C2PAInfo.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div v-if="c2pa">
|
||||
<div class="detail-title">
|
||||
{{ t("file_mode.content_credentials") }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="dateCreated"><v-icon>$vuetify.icons.ic_exif_time</v-icon>{{ dateCreated }}</div>
|
||||
<div class="detail-row" v-if="creator"><v-icon>$vuetify.icons.ic_exif_device_camera</v-icon>{{ creator }}</div>
|
||||
<div class="detail-row" v-if="aiInferenceResult?.aiGenerated">
|
||||
<v-icon>$vuetify.icons.ic_cc_ai</v-icon>{{ t("file_mode.ai_used") }}
|
||||
</div>
|
||||
|
||||
<!-- {{ JSON.stringify(props.c2pa, undefined, 4) }} -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AIInferenceResult, C2PAActionsAssertion, C2PAData } from "../../models/proof";
|
||||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
c2pa?: C2PAData;
|
||||
aiInferenceResult?: AIInferenceResult;
|
||||
}>();
|
||||
|
||||
const dateCreated = computed(() => {
|
||||
try {
|
||||
const manifests = Object.values(props.c2pa?.manifest_info.manifests ?? {});
|
||||
for (const manifest of manifests) {
|
||||
const createAssertion = manifest.assertions.find((a) => {
|
||||
if (a.label === "c2pa.actions") {
|
||||
const actions = (a.data as C2PAActionsAssertion)?.actions ?? [];
|
||||
if (actions.find((a) => a.action === "c2pa.created")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (createAssertion) {
|
||||
const d = dayjs(Date.parse(manifest.signature_info.time));
|
||||
return d.format("lll");
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const creator = computed(() => {
|
||||
try {
|
||||
const manifests = Object.values(props.c2pa?.manifest_info.manifests ?? {});
|
||||
for (const manifest of manifests) {
|
||||
for (const assertion of manifest.assertions) {
|
||||
if (assertion.label === "c2pa.actions") {
|
||||
const actions = (assertion.data as C2PAActionsAssertion)?.actions ?? [];
|
||||
const a = actions.find((a) => a.action === "c2pa.created");
|
||||
if (a) {
|
||||
return a.softwareAgent;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
113
src/components/content-credentials/EXIFInfo.vue
Normal file
113
src/components/content-credentials/EXIFInfo.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div v-if="exif">
|
||||
<div class="detail-title">
|
||||
{{ t("file_mode.exif_data") }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="dateTime">
|
||||
<v-icon>$vuetify.icons.ic_exif_time</v-icon>{{ dateTime }}
|
||||
</div>
|
||||
<div class="detail-row" v-if="location">
|
||||
<v-icon>$vuetify.icons.ic_exif_location</v-icon><a :href="locationLink">{{ location }}</a>
|
||||
</div>
|
||||
<div class="detail-row" v-if="makeAndModel">
|
||||
<v-icon>$vuetify.icons.ic_exif_device_camera</v-icon>{{ makeAndModel }}
|
||||
</div>
|
||||
<!-- Exif {{ JSON.stringify(props.exif, undefined, 4) }} -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
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");
|
||||
if (date) {
|
||||
return dayjs(Date.parse(date)).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,22 +28,35 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<C2PABadge :proof="attachment.proof" />
|
||||
<C2PAInfo class="attachment-info__detail-box" v-if="hasC2PA" :c2pa="attachment.proof?.integrity?.c2pa" :ai-inference-result="attachment.proof?.ai?.inferenceResult" />
|
||||
<EXIFInfo class="attachment-info__detail-box" v-if="hasExif" :exif="attachment.proof?.integrity?.exif" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Attachment } from "../../models/attachment";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import C2PABadge from "../c2pa/C2PABadge.vue";
|
||||
import C2PAInfo from "../content-credentials/C2PAInfo.vue";
|
||||
import EXIFInfo from "../content-credentials/EXIFInfo.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed } from "vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineProps<{
|
||||
const { attachment } = defineProps<{
|
||||
attachment: Attachment;
|
||||
}>();
|
||||
|
||||
//console.error("ATTACHMENT", attachment.proof);
|
||||
|
||||
const hasC2PA = computed(() => {
|
||||
return attachment.proof?.integrity?.c2pa !== undefined;
|
||||
});
|
||||
|
||||
const hasExif = computed(() => {
|
||||
return attachment.proof?.integrity?.exif !== undefined;
|
||||
});
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
return prettyBytes(bytes);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -214,14 +214,13 @@
|
|||
import { defineComponent, reactive } from "vue";
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import { Attachment } from "../../models/attachment";
|
||||
import C2PABadge from "../c2pa/C2PABadge.vue";
|
||||
import { createUploadBatch } from "../../models/attachmentManager";
|
||||
import AttachmentInfo from "./AttachmentInfo.vue";
|
||||
import ThumbnailView from "./ThumbnailView.vue";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [messageMixin],
|
||||
components: { C2PABadge, AttachmentInfo, ThumbnailView },
|
||||
components: { AttachmentInfo, ThumbnailView },
|
||||
emits: ["pick-file", "close"],
|
||||
props: {
|
||||
defaultRootMessageText: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue