Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions src/lib/OverviewPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,36 @@
return undefined
}

type Edge = { childIdx: number | null; relationship?: string; stubTitle?: string; stubFormat?: string }
type Edge = { childIdx: number | null; relationship?: string; stubTitle?: string; stubFormat?: string; stubThumbnailSrc?: string }

function ingredientThumbnailSrc(v: Record<string, unknown>, m: CrJsonManifestEntry): string | undefined {
const thumb = v.thumbnail
if (!thumb || typeof thumb !== 'object') return undefined
const t = thumb as Record<string, unknown>

// v2: inline data on the thumbnail object itself
if (typeof t.data === 'string' && t.data) {
const fmt = typeof t.format === 'string' ? t.format : 'image/jpeg'
const raw = t.data
const b64 = raw.startsWith("b64'") && raw.endsWith("'") ? raw.slice(4, -1) : raw
return `data:${fmt};base64,${b64}`
}

// v1: thumbnail.url is a JUMBF reference to a c2pa.thumbnail.* assertion in this manifest
// e.g. "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
const url = typeof t.url === 'string' ? t.url : undefined
if (!url) return undefined
const prefix = 'c2pa.assertions/'
const prefixIdx = url.indexOf(prefix)
if (prefixIdx < 0) return undefined
const assertionLabel = url.slice(prefixIdx + prefix.length)
const assertion = (m.assertions ?? {})[assertionLabel] as Record<string, unknown> | undefined
if (!assertion || typeof assertion.data !== 'string' || !assertion.data) return undefined
const fmt = typeof assertion.format === 'string' ? assertion.format : 'image/jpeg'
const raw = assertion.data
const b64 = raw.startsWith("b64'") && raw.endsWith("'") ? raw.slice(4, -1) : raw
return `data:${fmt};base64,${b64}`
}

function crJsonEdges(
m: CrJsonManifestEntry,
Expand All @@ -283,21 +312,22 @@
const relationship = v.relationship as string | undefined
const stubTitle = (v.title ?? v.dc_title) as string | undefined
const stubFormat = (v.format ?? v.dc_format) as string | undefined
const stubThumbnailSrc = ingredientThumbnailSrc(v, m)
// v1: c2pa_manifest is an object { url, alg, hash }
// v2: active_manifest is a direct string (manifest label)
const manifestRef = (v.c2pa_manifest ?? v.activeManifest) as Record<string, unknown> | undefined
const activeManifestStr = v['active_manifest'] as string | undefined
const url = (manifestRef?.url as string | undefined) ?? (typeof activeManifestStr === 'string' ? activeManifestStr : undefined)
if (!url) {
// No manifest reference — ingredient has no Content Credentials
out.push({ childIdx: null, relationship, stubTitle, stubFormat })
out.push({ childIdx: null, relationship, stubTitle, stubFormat, stubThumbnailSrc })
} else {
const childIdx = idx.get(parseManifestLabel(url))
if (childIdx != null) {
out.push({ childIdx, relationship })
} else {
// Manifest referenced but not present in this report
out.push({ childIdx: null, relationship, stubTitle, stubFormat })
out.push({ childIdx: null, relationship, stubTitle, stubFormat, stubThumbnailSrc })
}
}
}
Expand All @@ -309,7 +339,7 @@
manifestIdx: -1,
claimGenerator: edge.stubTitle,
mimeType: edge.stubFormat ?? null,
thumbnailSrc: undefined,
thumbnailSrc: edge.stubThumbnailSrc,
date: undefined,
ingredientCount: 0,
inceptions: [],
Expand Down
49 changes: 34 additions & 15 deletions src/lib/c2pa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ async function runTrustValidationFlow(
* The packaged SDK instance is cached so only one Web Worker is created.
*/
async function enrichThumbnailsViaPackagedSdk(crJson: CrJson, file: Blob, mimeType: string): Promise<void> {
// Quick check: any unresolved thumbnail identifiers?
// Quick check: any unresolved thumbnail identifiers? (manifest-level or ingredient-embedded)
let hasUnresolved = false
outer: for (const manifest of (crJson.manifests ?? [])) {
const assertions = (manifest.assertions ?? {}) as Record<string, Record<string, unknown>>
Expand All @@ -501,6 +501,13 @@ async function enrichThumbnailsViaPackagedSdk(crJson: CrJson, file: Blob, mimeTy
hasUnresolved = true
break outer
}
if (key.startsWith('c2pa.ingredient') && assertion) {
const thumb = assertion.thumbnail as Record<string, unknown> | undefined
if (thumb && !thumb.data && typeof thumb.identifier === 'string') {
hasUnresolved = true
break outer
}
}
}
}
if (!hasUnresolved) return
Expand All @@ -521,27 +528,39 @@ async function enrichThumbnailsViaPackagedSdk(crJson: CrJson, file: Blob, mimeTy

/**
* Resolve JUMBF `identifier` URIs in thumbnail assertions to inline base64 `data` fields.
* Handles both manifest-level (c2pa.thumbnail*) and ingredient-embedded (c2pa.ingredient*.thumbnail) thumbnails.
* Only runs when the reader exposes `resourceToBytes`; silently skips failures.
*/
async function enrichThumbnails(crJson: CrJson, resourceToBytes: (uri: string) => Promise<Uint8Array>): Promise<void> {
for (const manifest of (crJson.manifests ?? [])) {
const assertions = (manifest.assertions ?? {}) as Record<string, Record<string, unknown>>
for (const [key, assertion] of Object.entries(assertions)) {
if (!key.startsWith('c2pa.thumbnail') || !assertion || typeof assertion !== 'object') continue
if (assertion.data) continue // already inlined
const identifier = assertion.identifier
if (typeof identifier !== 'string') continue
try {
const bytes = await resourceToBytes(identifier)
// Convert to base64 in chunks to avoid call-stack limits on large thumbnails
const chunkSize = 8192
let binary = ''
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize))
if (!assertion || typeof assertion !== 'object') continue

const targets: Array<Record<string, unknown>> = []
if (key.startsWith('c2pa.thumbnail')) {
targets.push(assertion)
} else if (key.startsWith('c2pa.ingredient')) {
const thumb = assertion.thumbnail as Record<string, unknown> | undefined
if (thumb && typeof thumb === 'object') targets.push(thumb)
}

for (const target of targets) {
if (target.data) continue // already inlined
const identifier = target.identifier
if (typeof identifier !== 'string') continue
try {
const bytes = await resourceToBytes(identifier)
// Convert to base64 in chunks to avoid call-stack limits on large thumbnails
const chunkSize = 8192
let binary = ''
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize))
}
target.data = `b64'${btoa(binary)}'`
} catch {
// Non-fatal: skip thumbnails we can't resolve
}
assertion.data = `b64'${btoa(binary)}'`
} catch {
// Non-fatal: skip thumbnails we can't resolve
}
}
}
Expand Down