Skip to content

fix: restore ingredient thumbnails with c2pa-web#389

Open
timmurphydev wants to merge 8 commits into
contentauth:mainfrom
timmurphydev:fix/restore-ingredient-thumbnails
Open

fix: restore ingredient thumbnails with c2pa-web#389
timmurphydev wants to merge 8 commits into
contentauth:mainfrom
timmurphydev:fix/restore-ingredient-thumbnails

Conversation

@timmurphydev

Copy link
Copy Markdown

Overview

Restores ingredient thumbnail rendering. The c2pa-web SDK exposes raw resource bytes via reader.resourceToBytes() and we need to fetch and assign thumbnails explicitly.

  • Add resolveThumbnails helper that walks the manifest store and fetches embedded thumbnail bytes, keyed by absolute JUMBF identifier.
  • For ingredients with a manifest and claim thumbnail, we use its c2pa.thumbnail.claim assertion.
  • Otherwise we fall back to the c2pa.thumbnail.ingredient assertion on the containing manifest, if present.

@andyparsons andyparsons requested review from andyparsons May 6, 2026 23:48
@timmurphydev

Copy link
Copy Markdown
Author

This update prioritizes the signed ingredient's own c2pa.thumbnail.claim, if present,
over the c2pa.thumbnail.ingredient assertion that the consuming manifest's signer
references via ingredient.thumbnail. When a signed ingredient already provides
its own claim thumbnail, a consuming signer's separate ingredient assertion
thumbnail could be an intentional override pointing at a misleading image. The
claim thumbnail more faithfully represents the ingredient.

When no claim thumbnail is available for an ingredient, we fall back to
c2pa.thumbnail.ingredient only if the containing manifest is trusted. If it is
untrusted, we suppress the thumbnail so an untrusted signer can't force a false
thumbnail to display for an ingredient that has no claim thumbnail of its own. A
signed ingredient's own claim thumbnail is still shown when present (untrusted
state is flagged in the UI).

It solves this kind of issue:

Before:
Screenshot 2026-05-07 at 9 33 40 AM

After:
Screenshot 2026-05-07 at 9 34 07 AM

@timmurphydev timmurphydev marked this pull request as draft May 7, 2026 14:02
@timmurphydev timmurphydev marked this pull request as ready for review May 7, 2026 14:48
@timmurphydev

Copy link
Copy Markdown
Author

@andyparsons I synced this with main after #388 merged, and just updated c2pa-web to 0.9.0.

Comment thread package.json Outdated
@mauricefisher64

Copy link
Copy Markdown

Not a JS expert but looks good to me.

Comment thread src/lib/resolveThumbnails.ts Outdated
@@ -0,0 +1,66 @@
// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a new file this can be Copyright 2026 The C2PA contributors

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MassivaM Can this be // Copyright 2021-2024 Adobe, Copyright 2026 The C2PA Contributors instead? There is an eslint header rule that enforces this format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes I forgot we had that linter , we'll want to change this eventually but yes that's okay for now!

Comment thread src/lib/asset.ts Outdated
// untrusted, we suppress the thumbnail so an untrusted signer can't force a false
// thumbnail to display for an ingredient that has no claim thumbnail of its own. A
// signed ingredient's own claim thumbnail is still shown when present (untrusted
// state is flagged in the UI).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * The code prioritizes the signed ingredient's own c2pa.thumbnail.claim, if present,
 * over the c2pa.thumbnail.ingredient assertion that the consuming manifest's signer
 * references via ingredient.thumbnail. When a signed ingredient already provides
 * its own claim thumbnail, a consuming signer's separate ingredient assertion
 * thumbnail could be an intentional override pointing at a misleading image. The
 * claim thumbnail more faithfully represents the ingredient.
 *
 * When no claim thumbnail is available for an ingredient, we fall back to
 * c2pa.thumbnail.ingredient only if the containing manifest is trusted. If it is
 * untrusted, we suppress the thumbnail so an untrusted signer can't force a false
 * thumbnail to display for an ingredient that has no claim thumbnail of its own. A
 * signed ingredient's own claim thumbnail is still shown when present (untrusted
 * state is flagged in the UI)
 */

Can we use a multiline comment here to make it more readable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MassivaM I just fixed this. Thanks!

Comment thread src/lib/asset.ts
const containingManifestUntrusted = (runtimeValidationStatuses[containingManifestLabel] ?? [])
.some((s) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid'));

const thumbnail = ingredientManifestLabel && ingredientManifest?.thumbnail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are null guards needed here, in that chain of calls to set that variable?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tmathern,

const containingManifestUntrusted = (runtimeValidationStatuses[containingManifestLabel] ?? [])
  .some((s) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid'));

This is protected by the ?? []

const thumbnail = ingredientManifestLabel && ingredientManifest?.thumbnail
  ? await lookupThumbnail(ingredientManifest.thumbnail, ingredientManifestLabel)   // branch A
  : !containingManifestUntrusted
    ? await lookupThumbnail(ingredient.thumbnail, containingManifestLabel)         // branch B
    : await loadThumbnail(undefined, undefined);                                   // branch C

When this code runs, we already know that ingredient is not null. Branch A is guarded by ingredientManifestLabel && ingredientManifest?.thumbnail. On branch C, the existing loadThumbnail() is designed to accept undefined. I followed that same pattern in lookupThumbnail(). So in branch B, ingredient.thumbnail could be undefined, but lookupThumbnail() is null safe. It gets caught there by ref?.identifier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants