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
|
|
@ -472,6 +472,7 @@ $hiliteColor: #4642f1;
|
|||
}
|
||||
|
||||
.send-attachments-info-popup {
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
.done-button {
|
||||
padding: 14px 24px;
|
||||
}
|
||||
|
|
@ -531,7 +532,7 @@ $hiliteColor: #4642f1;
|
|||
background-color: #242424;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #242424;
|
||||
|
||||
|
||||
&.selected {
|
||||
border: 2px solid #4642F1;
|
||||
}
|
||||
|
|
@ -571,12 +572,32 @@ $hiliteColor: #4642f1;
|
|||
}
|
||||
}
|
||||
|
||||
.c2pa-badge {
|
||||
overflow: hidden;
|
||||
.attachment-info__detail-box {
|
||||
margin-top: 32px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: $backgroundDark;
|
||||
|
||||
.v-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
.detail-title {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
margin-top: 12px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 125%;
|
||||
letter-spacing: 0.4px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.v-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/assets/icons/ic_cc_ai.vue
Normal file
9
src/assets/icons/ic_cc_ai.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.7669 7.69299C10.4143 6.49122 9.50915 5.5853 8.30704 1.23383C8.26886 1.09544 8.14319 1 8 1C7.85682 1 7.73114 1.09544 7.69296 1.23383C6.49105 5.58597 5.58502 6.49102 1.23306 7.69299C1.09465 7.73117 1 7.85683 1 8C1 8.14317 1.09545 8.26883 1.23306 8.30701C5.5857 9.50878 6.49085 10.4147 7.69296 14.7662C7.73114 14.9046 7.85682 15 8 15C8.14319 15 8.26886 14.9046 8.30704 14.7662C9.50895 10.414 10.415 9.50898 14.7669 8.30701C14.9053 8.26883 15 8.14317 15 8C15 7.85683 14.9045 7.73117 14.7669 7.69299Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.9"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
20
src/assets/icons/ic_exif_device_camera.vue
Normal file
20
src/assets/icons/ic_exif_device_camera.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.33337 5.58483C1.33337 5.35128 1.33337 5.23451 1.34312 5.13615C1.43711 4.18752 2.18755 3.43708 3.13618 3.34309C3.23454 3.33334 3.35761 3.33334 3.60376 3.33334C3.69861 3.33334 3.74603 3.33334 3.7863 3.33091C4.30045 3.29977 4.75068 2.97526 4.9428 2.49734C4.95785 2.45992 4.97191 2.41772 5.00004 2.33334C5.02817 2.24896 5.04223 2.20677 5.05728 2.16934C5.24941 1.69143 5.69963 1.36692 6.21378 1.33578C6.25405 1.33334 6.29852 1.33334 6.38747 1.33334H9.61262C9.70156 1.33334 9.74603 1.33334 9.7863 1.33578C10.3004 1.36692 10.7507 1.69143 10.9428 2.16934C10.9579 2.20677 10.9719 2.24896 11 2.33334C11.0282 2.41772 11.0422 2.45992 11.0573 2.49734C11.2494 2.97526 11.6996 3.29977 12.2138 3.33091C12.254 3.33334 12.3015 3.33334 12.3963 3.33334C12.6425 3.33334 12.7655 3.33334 12.8639 3.34309C13.8125 3.43708 14.563 4.18752 14.657 5.13615C14.6667 5.23451 14.6667 5.35128 14.6667 5.58483V10.8C14.6667 11.9201 14.6667 12.4802 14.4487 12.908C14.257 13.2843 13.951 13.5903 13.5747 13.782C13.1469 14 12.5868 14 11.4667 14H4.53337C3.41327 14 2.85322 14 2.42539 13.782C2.04907 13.5903 1.74311 13.2843 1.55136 12.908C1.33337 12.4802 1.33337 11.9201 1.33337 10.8V5.58483Z"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="0.9"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.00004 11C9.4728 11 10.6667 9.8061 10.6667 8.33334C10.6667 6.86058 9.4728 5.66668 8.00004 5.66668C6.52728 5.66668 5.33337 6.86058 5.33337 8.33334C5.33337 9.8061 6.52728 11 8.00004 11Z"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="0.9"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
20
src/assets/icons/ic_exif_location.vue
Normal file
20
src/assets/icons/ic_exif_location.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.99996 8.66668C9.10453 8.66668 9.99996 7.77125 9.99996 6.66668C9.99996 5.56211 9.10453 4.66668 7.99996 4.66668C6.89539 4.66668 5.99996 5.56211 5.99996 6.66668C5.99996 7.77125 6.89539 8.66668 7.99996 8.66668Z"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="0.9"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.99996 14.6667C10.6666 12 13.3333 9.6122 13.3333 6.66668C13.3333 3.72116 10.9455 1.33334 7.99996 1.33334C5.05444 1.33334 2.66663 3.72116 2.66663 6.66668C2.66663 9.6122 5.33329 12 7.99996 14.6667Z"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="0.9"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
12
src/assets/icons/ic_exif_time.vue
Normal file
12
src/assets/icons/ic_exif_time.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14 6.66668H2M10.6667 1.33334V4.00001M5.33333 1.33334V4.00001M5.2 14.6667H10.8C11.9201 14.6667 12.4802 14.6667 12.908 14.4487C13.2843 14.2569 13.5903 13.951 13.782 13.5747C14 13.1468 14 12.5868 14 11.4667V5.86668C14 4.74657 14 4.18652 13.782 3.7587C13.5903 3.38237 13.2843 3.07641 12.908 2.88466C12.4802 2.66668 11.9201 2.66668 10.8 2.66668H5.2C4.0799 2.66668 3.51984 2.66668 3.09202 2.88466C2.71569 3.07641 2.40973 3.38237 2.21799 3.7587C2 4.18652 2 4.74657 2 5.86668V11.4667C2 12.5868 2 13.1468 2.21799 13.5747C2.40973 13.951 2.71569 14.2569 3.09202 14.4487C3.51984 14.6667 4.0799 14.6667 5.2 14.6667Z"
|
||||
stroke="currentColor"
|
||||
stroke-opacity="0.9"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -488,6 +488,9 @@
|
|||
"compressed": "Compressed",
|
||||
"original": "Original",
|
||||
"metadata_info_compressed": "Compressing the image automatically excludes its metadata.",
|
||||
"metadata_info_original": "Sharing the original automatically includes its metadata."
|
||||
"metadata_info_original": "Sharing the original automatically includes its metadata.",
|
||||
"exif_data": "Exif Data",
|
||||
"content_credentials": "Content Credentials",
|
||||
"ai_used": "AI was used in this media"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ComputedRef, Ref } from "vue";
|
||||
import { Proof } from "./proof";
|
||||
|
||||
export class UploadPromise<Type> {
|
||||
wrappedPromise: Promise<Type>;
|
||||
|
|
@ -48,7 +49,7 @@ export type Attachment = {
|
|||
scaledDimensions?: { width: number; height: number };
|
||||
useScaled: boolean;
|
||||
src?: string;
|
||||
proof?: any;
|
||||
proof?: Proof;
|
||||
sendInfo?: AttachmentSendInfo;
|
||||
};
|
||||
|
||||
|
|
|
|||
40
src/models/proof.ts
Normal file
40
src/models/proof.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
export type AIInferenceResult = {
|
||||
aiGenerated: boolean;
|
||||
aiProbability: number;
|
||||
humanProbability: number;
|
||||
}
|
||||
|
||||
export type C2PAActionsAssertion = {
|
||||
actions: {
|
||||
action: string;
|
||||
softwareAgent?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type C2PAAssertion = {
|
||||
label: string;
|
||||
data: C2PAActionsAssertion | undefined;
|
||||
}
|
||||
|
||||
export type C2PAManifest = {
|
||||
assertions: C2PAAssertion[];
|
||||
signature_info: {
|
||||
time: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type C2PAManifestInfo = {
|
||||
active_manifest: string;
|
||||
manifests: {[key: string]: C2PAManifest};
|
||||
}
|
||||
|
||||
export type C2PAData = {
|
||||
manifest_info: C2PAManifestInfo;
|
||||
}
|
||||
|
||||
export type Proof = {
|
||||
name?: string;
|
||||
json?: string;
|
||||
integrity?: { pgp?: any; c2pa?: any; exif?: {[key: string]: string | Object}; opentimestamps?: any };
|
||||
ai?: { inferenceResult?: AIInferenceResult};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue