Skip to content
Merged
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
105 changes: 91 additions & 14 deletions docs/selective-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,8 @@ new_builder.sign(source_path, output_path, signer);

An ingredient archive is a serialized `Builder` containing exactly one and only one ingredient (see [Builder archives vs. ingredient archives](#builder-archives-vs-ingredient-archives)). Reading it with `Reader` allows the caller to inspect the ingredient before deciding whether to use it: its thumbnail, whether it carries provenance (e.g. an active manifest), validation status, relationship, etc.

Reading is independent of linking, see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action) for how to attach the ingredient to an action without reading it first.

```mermaid
flowchart LR
IA["ingredient_archive.c2pa"] -->|"Reader(application/c2pa)"| JSON["JSON + resources"]
Expand All @@ -739,7 +741,7 @@ auto parsed = json::parse(reader.json());
std::string active = parsed["active_manifest"];
auto manifest = parsed["manifests"][active];

// An ingredient archive has exactly one ingredient
// An ingredient archive must always contain exactly one ingredient
auto& ingredient = manifest["ingredients"][0];

// Relationship
Expand Down Expand Up @@ -781,25 +783,34 @@ if (ingredient.contains("thumbnail")) {
}
```

#### Ingredient vs. ingredient archive

A plain ingredient is a source asset (image, video, document) the builder reads at `add_ingredient` time, with `label` (primary) or `instance_id` (fallback) usable as linking keys. An ingredient archive is a `.c2pa` file containing one already-formed ingredient. When passed to `add_ingredient`, the builder treats its contents as opaque provenance. The only linking key the action can resolve is the `label` set on the *current* `add_ingredient` call.

For a side-by-side comparison, see [Ingredient vs. ingredient archive](working-stores.md#ingredient-vs-ingredient-archive) in the working-stores doc.

#### Linking an archived ingredient to an action

After reading the ingredient details from an ingredient archive, the ingredient can be added to a new `Builder` and linked to an action. You must assign a `label` in the `add_ingredient` call on the signing builder and use that label as the linking key in `ingredientIds`. Labels baked into the archive ingredient are not carried through, and `instance_id` does not work as a linking key for ingredient archives.
Linking an **archived** ingredient to an action is **label-driven**: archived ingredients can only be linked to actions using labels.

To do so, set a `label` on the archived ingredient's JSON passed to `add_ingredient` on the builder, and use that same string in the action's `ingredientIds`.

Reading the archive first is *not* required to link it. `Reader` is only useful when the caller wants to preview the ingredient (thumbnail, provenance, validation status) before deciding whether to use it (see [Reading ingredient details from an ingredient archive](#reading-ingredient-details-from-an-ingredient-archive)).

> [!WARNING]
> **`instance_id` does not work as a linking key for ingredient archives.** Use `label` instead.
>
> **Labels baked into the archive ingredient at archive-creation time do not carry through as linking keys either.** The label must be re-asserted on the signing builder's `add_ingredient` call so action and archived ingredient properly link.

Note that labels are only used as build-time linking keys. The SDK may reassign the actual label in the signed manifest.
Labels are build-time linking keys only. A label, as linking key, links ingredients and actions using it together: the label identifies the link. The SDK may reassign the actual label in the signed manifest.

Assign a `label` in the `add_ingredient` call and reference that same label in `ingredientIds`. This works whether or not the ingredient has an `instance_id`.
##### Minimal archive to action linking example

Build a manifest whose action references a chosen label, then `add_ingredient` with that label on the signing builder. No `Reader`, no parsing of the archive:

```cpp
c2pa::Context context;

// Read the ingredient archive
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
c2pa::Reader reader(context, "application/c2pa", archive_file);
auto parsed = json::parse(reader.json());
std::string active = parsed["active_manifest"];
auto& ingredient = parsed["manifests"][active]["ingredients"][0];

// Use a caller-assigned label as the linking key
json manifest_json = {
{"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})},
{"assertions", json::array({
Expand All @@ -821,20 +832,86 @@ json manifest_json = {

c2pa::Builder builder(context, manifest_json.dump());

// The label on the ingredient JSON matches the entry in ingredientIds
// Same label string as in ingredientIds above: that is what links the action.
builder.add_ingredient(
R"({
"title": "photo.jpg",
"relationship": "parentOf",
"label": "archived-ingredient"
})",
archive_path);

// Note that a signing, the SDK may reassign the labels
builder.sign(source_path, output_path, signer);
```

The `add_ingredient` overload that takes a `std::istream` (with `"application/c2pa"` as the format) follows the same rules: the `label` on the ingredient JSON is what links the action:

```cpp
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
c2pa::Builder builder(context, manifest_json.dump());

builder.add_ingredient(
R"({
"title": "photo.jpg",
"relationship": "parentOf",
"label": "archived-ingredient"
})",
"application/c2pa",
archive_file);

builder.sign(source_path, output_path, signer);
```

##### Previewing the archive before linking

If you want to inspect the ingredient archive (e.g. to decide whether to use it, or to copy a `title` from it), open it with `Reader` first, then add it as an ingredient. The Reader step is independent of the linking: the link is still established by the `label` on the signing builder's `add_ingredient` call.

```cpp
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
c2pa::Reader reader(context, "application/c2pa", archive_file);
auto parsed = json::parse(reader.json());
std::string active = parsed["active_manifest"];
auto& ingredient = parsed["manifests"][active]["ingredients"][0];

// Inspect ingredient (thumbnail, validation_status, active_manifest, etc.)
// before deciding to use it. See "Reading ingredient details from an ingredient archive".

c2pa::Builder builder(context, manifest_json.dump());

archive_file.seekg(0);
builder.add_ingredient(
json({
{"title", ingredient["title"]},
{"relationship", "parentOf"},
{"label", "archived-ingredient"}
{"label", "archived-ingredient"} // The linking key: chosen here, not read from the archive.
}).dump(),
"application/c2pa",
archive_file);

builder.sign(source_path, output_path, signer);
```

#### Troubleshooting linking errors

A common signing-time error when linking ingredients is:

```text
Builder.sign failure: Other: assertion-specific error:
Action ingredientId not found: <some id>
```

Causes and potential fixes to investigate:

| Symptom | Cause | Fix |
| --- | --- | --- |
| `Action ingredientId not found: xmp:iid:...` (or any `instance_id` value) | `instance_id` was used as the linking key for an ingredient archive. | Assign a `label` on the signing builder's `add_ingredient` JSON, and use that label in `ingredientIds`. |
| `Action ingredientId not found: <label>` where the label was set only when building the archive | Labels baked into an archive ingredient do not carry through as linking keys. | Re-assert the same `label` in the signing builder's `add_ingredient` JSON when calling `add_ingredient`. |
| `Action ingredientId not found: <label>` where the label is on `add_ingredient` but the action references a different string | Typo or mismatch between `ingredientIds[i]` and the `label` field on the ingredient. | Make the two strings identical, as the string is a linking key. |
| Sign succeeds but the action's `parameters.ingredients` array is empty in the signed output | The action was kept during a filter/rebuild but the corresponding ingredient was not, or was not linked. | See [Filtering actions that reference ingredients](#filtering-actions-that-reference-ingredients) to keep the ingredient and its binary resources alongside the action. Verify that the linking of ingredients and actions uses the correct JSON attributes. |

For the linking rules, see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action) above.

### Merging multiple working stores

In some cases you may need to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**—the recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated.
Expand Down
43 changes: 40 additions & 3 deletions docs/working-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,21 @@ builder.sign("source.jpg", "signed.jpg", signer);

Ingredients represent source materials used to create an asset, preserving the provenance chain. Ingredients themselves can be turned into ingredient archives (`.c2pa`).

An ingredient archive is a serialized `Builder` with _exactly one_ ingredient. Once archived with only one ingredient, the Builder archive is an ingredient archive. Such ingredient archives can be used as ingredient in other working stores.
### Ingredient vs. ingredient archive

A **(plain) ingredient** is a source asset that the builder reads at `add_ingredient` time. The builder sees the asset's bytes, and stores live required ingredient data (including any caller-set `instance_id`) inside the new manifest.

An **ingredient archive** (in c2pa-archive-format) is a `.c2pa` file produced by `to_archive()` that already contains a fully-formed ingredient ("a ready to use ingredient"). When passed to `add_ingredient`, the builder treats the archive's contents as opaque provenance: the archive's internal fields are not exposed as live JSON the signing builder can introspect (or use for linking to actions). Only the JSON the caller supplies in the current `add_ingredient` call is visible to the builder in that round.

This difference governs how each can be linked to an action via `ingredientIds`:

| Aspect | Ingredient | Ingredient archive |
| --- | --- | --- |
| Source format passed to `add_ingredient` | Asset MIME type (`image/jpeg`, `video/mp4`, ...) or asset path | `application/c2pa` or path to a `.c2pa` ingredient archive file |
| What it is | "Live" asset | A serialized manifest store (opaque provenance) |
| Linking via `label` | Primary linking key, set on the signing builder's `add_ingredient` JSON parameter | Only linking key that works, set on the signing builder's `add_ingredient` JSON |
| Linking via `instance_id` | Alternative to using `label` | Does not link, signing-time error |
| Linking via a `label` baked in at archive-creation time | N/A (not an archive) | Does not carry through, must be re-asserted on the signing builder, set on the signing builder's `add_ingredient` JSON parameter |

### Adding ingredients to a working store

Expand Down Expand Up @@ -479,7 +493,17 @@ builder.sign("new_asset.jpg", "signed_asset.jpg", signer);

### Linking an ingredient archive to an action

To link an ingredient archive to an action via `ingredientIds`, you must use a `label` set in the `add_ingredient` call on the signing builder. Labels baked into the archive ingredient are not carried through, and `instance_id` does not work as a linking key for ingredient archives regardless of where it is set.
> [!IMPORTANT]
> **Linking an ingredient archive is `label`-driven only.**
>
> - `instance_id` does not work as a linking key for ingredient archives, use `label` instead.
> - Labels baked into the archive at archive-creation time do not carry through. The label must be re-asserted in the signing builder's `add_ingredient` JSON.
> - Both rules apply whether the archive is added by file path or by stream.
>
> Attempting to link via `instance_id`, or relying on a baked-in label alone, produces a sign-time error: `Action ingredientId not found: <id>`. See [Troubleshooting linking errors](#troubleshooting-linking-errors).

To link an ingredient archive to an action via `ingredientIds`, set a `label` on the JSON passed to `add_ingredient` on the signing builder, and use the same string in the action's `ingredientIds` array. A label, as linking key, links ingredients and actions using it together: the label identifies the link. Labels are build-time linking keys only. The SDK may reassign the actual label in the signed manifest during signing.


```cpp
c2pa::Context context;
Expand Down Expand Up @@ -519,6 +543,19 @@ builder.add_ingredient(
builder.sign("source.jpg", "signed.jpg", signer);
```

The stream overload of `add_ingredient` (with `"application/c2pa"` as the format) accepts the same `label`-based linking: open the archive as a `std::ifstream` and pass it instead of the file path:

```cpp
std::ifstream archive_stream("ingredient.c2pa", std::ios::binary);
builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"})",
"application/c2pa",
archive_stream);
archive_stream.close();

builder.sign("source.jpg", "signed.jpg", signer);
```

When linking multiple ingredient archives, give each a distinct label and reference it in the appropriate action's `ingredientIds` array.

If each ingredient has its own action (e.g., one `c2pa.opened` for the parent and one `c2pa.placed` for a composited element), set up two actions with separate `ingredientIds`:
Expand Down Expand Up @@ -609,7 +646,7 @@ const std::string ingredient_json = R"({
builder.add_ingredient(ingredient_json, "base_layer.png");
```

## Working with archives
## Working with archives

An *archive* (C2PA archive) is a serialized working store (`Builder` object) saved to a file or stream.

Expand Down
Loading
Loading