Skip to content
Merged
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
119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,125 @@ const ingredient = builder.addIngredientFromReader(sourceReader);
console.log(ingredient.title); // Contains ingredient metadata
```

#### Adding Ingredients from Archives (.c2pa files)

You can add ingredients from `.c2pa` archive files. Archives are binary files that contain a manifest store with ingredients and their associated resources (thumbnails, manifest data, etc.). To work with them, read the archive with `Reader` using the `application/c2pa` MIME type, then extract the ingredients and transfer their binary resources to a new `Builder`.

There are two types of archives sharing the same binary format:

- **Builder archives** (working store archives): Serialized snapshots of a `Builder`, created by `builder.toArchive()`. They can contain multiple ingredients.
- **Ingredient archives**: Contain exactly one ingredient from a source asset. They carry the provenance history for reuse in other manifests.

##### Reading an archive and adding its ingredients
Copy link
Contributor

Choose a reason for hiding this comment

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

So it seems like there's to analogous way to add an ingredient from Blob like in the JS SDK with addIngredientFromBlob. 🤔 Is that right?

If so, that feels like something we should definitely revisit when we look into refactoring and cleaning up these two SDKs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. The problem with Blob is that it isn't supported in older versions of Node. We can probably add it now.


```javascript
import { Reader, Builder } from '@contentauth/c2pa-node';
import * as fs from 'node:fs/promises';

// Read the archive using the application/c2pa MIME type
const archiveBuffer = await fs.readFile('ingredients.c2pa');
const reader = await Reader.fromAsset(
{ buffer: archiveBuffer, mimeType: 'application/c2pa' },
{ verify: { verify_after_reading: false } }
);

// Get the ingredients from the active manifest
const activeManifest = reader.getActive();
const ingredients = activeManifest.ingredients;

// Create a new builder with the ingredients from the archive
const builder = Builder.withJson({
claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],
ingredients: ingredients,
});

// Transfer binary resources (thumbnails, manifest_data) for each ingredient
for (const ingredient of ingredients) {
if (ingredient.thumbnail) {
const resource = await reader.resourceToAsset(ingredient.thumbnail.identifier, { buffer: null });
await builder.addResource(ingredient.thumbnail.identifier, {
buffer: resource.buffer,
mimeType: ingredient.thumbnail.format,
});
}
if (ingredient.manifest_data) {
const resource = await reader.resourceToAsset(ingredient.manifest_data.identifier, { buffer: null });
await builder.addResource(ingredient.manifest_data.identifier, {
buffer: resource.buffer,
mimeType: 'application/c2pa',
});
}
}

// Sign the manifest
const signer = LocalSigner.newSigner(cert, key, 'es256');
builder.sign(signer, inputAsset, outputAsset);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this might be missing an await just to make it more clear that it's async.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The LocalSigner is synchronous.

```

##### Selecting specific ingredients from an archive

When an archive contains multiple ingredients, you can filter to include only the ones you need:

```javascript
const reader = await Reader.fromAsset(
{ buffer: archiveBuffer, mimeType: 'application/c2pa' },
{ verify: { verify_after_reading: false } }
);

const activeManifest = reader.getActive();
const allIngredients = activeManifest.ingredients;

// Select only the ingredients you want (e.g., by title or instance_id)
const selected = allIngredients.filter(
(ing) => ing.title === 'photo_1.jpg' || ing.instance_id === 'catalog:logo'
);

// Build with only the selected ingredients
const builder = Builder.withJson({
claim_generator_info: [{ name: 'my-app', version: '1.0.0' }],
ingredients: selected,
});

// Transfer resources only for selected ingredients
for (const ingredient of selected) {
if (ingredient.thumbnail) {
const resource = await reader.resourceToAsset(ingredient.thumbnail.identifier, { buffer: null });
await builder.addResource(ingredient.thumbnail.identifier, {
buffer: resource.buffer,
mimeType: ingredient.thumbnail.format,
});
}
if (ingredient.manifest_data) {
const resource = await reader.resourceToAsset(ingredient.manifest_data.identifier, { buffer: null });
await builder.addResource(ingredient.manifest_data.identifier, {
buffer: resource.buffer,
mimeType: 'application/c2pa',
});
}
}
```

##### Building an ingredient archive

To create an ingredient archive, add ingredients to a `Builder` and save it as an archive:

```javascript
const builder = Builder.new();

// Add ingredients with stable instance_id for later catalog lookups
await builder.addIngredient(
JSON.stringify({
title: 'photo-A.jpg',
relationship: 'componentOf',
instance_id: 'catalog:photo-A',
}),
{ path: 'photo-A.jpg' }
);

// Save as a .c2pa archive
await builder.toArchive({ path: 'ingredient-catalog.c2pa' });
```

#### Creating and Reusing Builder Archives

Builder archives allow you to save a builder's state (including ingredients) and reuse it later:
Expand Down
Loading