Skip to content

Commit

Permalink
feat(src): add content credential pin support
Browse files Browse the repository at this point in the history
Add support for a content credential pin to display the C2PA
verification result on the C2PA verify page.
  • Loading branch information
olgahaha committed Jan 23, 2025
1 parent 8a6b6b0 commit d5f4777
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 2 deletions.
36 changes: 36 additions & 0 deletions src/asset/asset-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import { Constant } from '../constant';
import { AssetModel, CaptureEyeCustomItem } from './asset-model';

export async function downloadC2pa(
nid: string, scopeToken: string | null
): Promise<string | undefined> {
if (!scopeToken) {
console.log('Skip downloading C2PA');
return;
}

try {
const headers = {
Authorization: `Bearer ${scopeToken}`,
};

const response = await fetch(`${Constant.url.assetApi}${nid}/c2pa/`, {
method: 'POST',
headers: headers,
});

if (!response.ok) {
if (response.status >= 400 && response.status < 500) {
const errorResponse = await response.json();
throw new Error(
`HTTP ${response.status}: ` +
`${errorResponse?.error?.type} ${errorResponse?.error?.message}`
);
}
throw new Error(`HTTP error! Status: ${response.status}`);
}
const { url } = await response.json();
return url as string;
} catch (error) {
console.error('Download C2PA error:', error);
return;
}
}

export async function fetchAsset(nid: string): Promise<AssetModel | undefined> {
const headers: HeadersInit = {
'Content-Type': 'application/json',
Expand Down
2 changes: 1 addition & 1 deletion src/modal/interaction-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class InteractionTracker {
static TOKEN_CRYPTO_ALGORITHM = 'AES-GCM';
private domain: string;
private path: string;
private token: string | null;
token: string | null;

constructor() {
this.domain = window.location.hostname;
Expand Down
31 changes: 31 additions & 0 deletions src/modal/modal-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,43 @@ export function getModalStyles() {
gap: 4px;
}
.badge-container div,
.badge-container img {
position: relative;
width: 32px;
height: 32px;
display: block;
}
.button-content-credentials {
cursor: pointer;
}
.button-content-credentials svg {
width: 100%;
height: 100%;
}
.button-content-credentials.loading::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 3px;
background-color: var(--primary-color);
animation: button-content-credentials-loading 2s infinite;
}
@keyframes button-content-credentials-loading {
0% {
width: 0;
}
100% {
width: 50%;
}
}
.profile-container {
display: flex;
align-items: center;
Expand Down
92 changes: 92 additions & 0 deletions src/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { customElement, property, state, query } from 'lit/decorators.js';
import { getModalStyles } from './modal-styles.js';
import { Constant } from '../constant.js';
import { AssetModel } from '../asset/asset-model.js';
import { downloadC2pa } from '../asset/asset-service.js';
import interactionTracker, { TrackerEvent } from './interaction-tracker.js';
import { isMobile } from '../utils.js';

Expand Down Expand Up @@ -84,6 +85,8 @@ export class CaptureEyeModal extends LitElement {

@query('.modal') modalElement!: HTMLDivElement;

private _c2paNid = '';

constructor() {
super();
}
Expand Down Expand Up @@ -212,6 +215,31 @@ export class CaptureEyeModal extends LitElement {
this.startEngagementZoneRotation();
}

private isC2paSupported() {
return typeof this._asset?.encodingFormat === 'string' && new Set([
'video/msvideo',
'video/avi',
'application-msvideo',
'image/avif',
'application/x-c2pa-manifest-store',
'image/x-adobe-dng',
'image/heic',
'image/heif',
'image/jpeg',
'audio/mp4',
'audio/mpeg',
'video/mp4',
'application/mp4',
'video/quicktime',
'application/pdf',
'image/png',
'image/svg+xml',
'image/tiff',
'audio/x-wav',
'image/webp',
]).has(this._asset?.encodingFormat);
}

private isOriginal() {
return this.layout == Constant.layout.original;
}
Expand All @@ -225,9 +253,34 @@ export class CaptureEyeModal extends LitElement {
/>`
: html``;

const contentCredentials = this.isC2paSupported()
? html`<div
class="button-content-credentials" title="Inspect Content Credentials"
@click=${this.handleInspectContentCredentials}
>
<svg viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M14.6,8v6.6H8c-3.7,0-6.6-3-6.6-6.6s3-6.6,6.6-6.6S14.6,4.3,14.6,8z
M0,8c0-4.4,3.6-8,8-8s8,3.6,8,8v8H8 C3.6,16,0,12.4,0,8z
M3.2,8.3c0,1.6,1.1,3,2.9,3c1.5,0,2.4-1,2.7-2.2H7.3c-0.2,0.6-0.6,
0.9-1.2,0.9c-0.9,0-1.5-0.7-1.5-1.8
s0.6-1.8,1.5-1.8c0.6,0,1,0.3,1.2,0.9h1.4C8.5,6.2,7.5,5.3,6.1,5.3C4.3,
5.3,3.2,6.7,3.2,8.3z
M10.7,5.4H9.3v5.8h1.4v-3
c0-0.6,0.2-0.9,0.4-1.2c0.2-0.2,0.6-0.3,1.1-0.3h0.4V5.4h-0.4c-0.8,0-1.2,
0.3-1.6,0.7L10.7,5.4L10.7,5.4z"
clip-rule="evenodd"
>
</path>
</svg>
</div>`
: html``;

return html`
<div class="badge-container">
${generatedViaAi}
${contentCredentials}
</div>
`;
}
Expand Down Expand Up @@ -547,6 +600,45 @@ export class CaptureEyeModal extends LitElement {
}
}

private handleInspectContentCredentials() {
if (this._c2paNid) {
window.open(this._c2paNid);
return;
}

const button = this.shadowRoot?.querySelector('.button-content-credentials') as HTMLElement;
if (button.classList.contains('loading')) {
return;
}

if (!confirm(
'Inspecting Content Credentials might take a little while. Proceed?'
)) {
return;
}

button.classList.add('loading');
button.title = 'Inspecting Content Credentials...';

downloadC2pa(this.nid, interactionTracker.token).then((url) => {
if (!this.isConnected) {
return;
}

if (url) {
this._c2paNid = `https://contentcredentials.org/verify?source=${url}`;
button.title = 'View Content Credentials';
alert(
'Data is ready. Please click the Content Credential pin again to view it.'
);
} else {
button.title = 'Inspect Content Credentials';
alert('Something went wrong. Please try again later.');
}
button.classList.remove('loading');
})
}

private emitRemoveEvent() {
// Emit remove event to trigger ModalManager to remove the modal
this.dispatchEvent(new CustomEvent('remove-capture-eye-modal'));
Expand Down
5 changes: 4 additions & 1 deletion src/test/modal_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ suite('capture-eye-modal', () => {
test('renders asset correctly when asset is loaded', async () => {
const assetData: AssetModel = {
creator: 'John Doe',
encodingFormat: 'image/jpeg',
thumbnailUrl: 'https://example.com/thumbnail.jpg',
captureTime: '2024-10-16',
captureLocation: 'New York, USA',
Expand Down Expand Up @@ -454,8 +455,10 @@ suite('capture-eye-modal', () => {
const badge = el.shadowRoot?.querySelector(
'div.badge-container img[alt="Generated via AI"]'
);

expect(badge).to.null;

const buttonCr = el.shadowRoot?.querySelector('.button-content-credentials');
expect(buttonCr).to.exist;
});

test('should render generated via AI correctly', async () => {
Expand Down

0 comments on commit d5f4777

Please sign in to comment.