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
108 changes: 51 additions & 57 deletions docs/selective-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,42 @@ for (auto& ingredient : selected) {
builder.sign(source_path, output_path, signer);
```

### Overriding ingredient properties
### Identifying ingredients in archives

When building an ingredient archive, you can set `instance_id` on the ingredient to give it a stable, caller-controlled identifier. This field survives archiving and signing unchanged, so it can be used to look up a specific ingredient from a catalog archive. The `description` and `informational_URI` fields also survive and can carry additional metadata about the ingredient's origin.

`instance_id` is only for identification and catalog lookups. It cannot be used as a linking key in `ingredientIds` when linking ingredient archives to actions — use `label` for that (see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action)).

```cpp
// Set instance_id when adding the ingredient to the archive builder.
auto builder = c2pa::Builder(context, manifest_str);
builder.add_ingredient(
R"({
"title": "photo-A.jpg",
"relationship": "componentOf",
"instance_id": "catalog:photo-A"
})",
source_path);

builder.to_archive("catalog.c2pa");
```

Later, when reading the archive, select ingredients by their `instance_id`:

```cpp
auto reader = c2pa::Reader(context, "catalog.c2pa");
auto parsed = json::parse(reader.json());
std::string active = parsed["active_manifest"];
auto& ingredients = parsed["manifests"][active]["ingredients"];

for (auto& ing : ingredients) {
if (ing.contains("instance_id") && ing["instance_id"] == "catalog:photo-A") {
// Do something with the found ingredient...
}
}
```

### Overriding ingredient properties

When adding an ingredient from an archive or from a file, the JSON passed to `add_ingredient()` can override properties like `title` and `relationship`. This is useful when reusing archived ingredients in a different context:

Expand Down Expand Up @@ -748,11 +783,9 @@ if (ingredient.contains("thumbnail")) {

#### 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. The preferred approach is to assign a `label` in the `add_ingredient` call and use that label as the linking key in `ingredientIds`. If the archived ingredient carries an `instance_id`, you can use that instead.
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.

Note that labels are only used as build-time linking keys. The SDK may reassign the actual label in the signed manifest. An `instance_id`, on the other hand, is preserved as-is through signing and can be read back unchanged from the final manifest.

##### Using a label
Note that labels are only used as build-time linking keys. 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`.

Expand Down Expand Up @@ -802,55 +835,6 @@ builder.add_ingredient(
builder.sign(source_path, output_path, signer);
```

##### Using an `instance_id`

If the ingredient archive carries an `instance_id` and you need a stable identifier that persists unchanged in the signed manifest, you can use the `instance_id` as the linking key in `ingredientIds` instead of a label.

```cpp
c2pa::Context context;

// Read the ingredient archive and extract the instance_id
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];
std::string instance_id = ingredient["instance_id"];

json manifest_json = {
{"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})},
{"assertions", json::array({
{
{"label", "c2pa.actions.v2"},
{"data", {
{"actions", json::array({
{
{"action", "c2pa.placed"},
{"parameters", {
{"ingredientIds", json::array({instance_id})}
}}
}
})}
}}
}
})}
};

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

archive_file.seekg(0);
builder.add_ingredient(
json({
{"title", ingredient["title"]},
{"relationship", "componentOf"},
{"instance_id", instance_id}
}).dump(),
"application/c2pa",
archive_file);

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

### 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 Expand Up @@ -1226,12 +1210,22 @@ auto manifest_bytes = builder.sign("image/jpeg", source, dest, signer);
// The output asset has no embedded manifest
```

Reading back:
### Checking manifest location on a Reader

After opening an asset with `Reader`, use `is_embedded()` to check whether the manifest is embedded in the asset or stored remotely. If the manifest is remote, `remote_url()` returns the URL it was fetched from (the URL set via `set_remote_url()` at signing time).

```cpp
c2pa::Reader reader(context, "image/jpeg", dest);
reader.is_embedded(); // false
reader.remote_url(); // "<<URI/URL to remote storage of manifest bytes>>"

if (reader.is_embedded()) {
std::cout << "Manifest is embedded in the asset." << std::endl;
} else {
std::cout << "Manifest is not embedded." << std::endl;
auto url = reader.remote_url();
if (url.has_value()) {
std::cout << "Remote manifest URL: " << url.value() << std::endl;
}
}
```

## Complete workflow diagram
Expand Down
111 changes: 111 additions & 0 deletions docs/working-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,117 @@ ingredient_stream.close();
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.

```cpp
c2pa::Context context;

// Step 1: Create the ingredient archive.
auto manifest_str = read_file("training.json");
auto archive_builder = c2pa::Builder(context, manifest_str);
archive_builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "componentOf"})",
"photo.jpg");
archive_builder.to_archive("ingredient.c2pa");

// Step 2: Build a manifest with an action that references the ingredient.
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["my-ingredient"]
}
}]
}
}]
})";

auto builder = c2pa::Builder(context, manifest_json);

// Step 3: Add the ingredient archive with a label matching the ingredientIds value.
// The label MUST be set here, on the signing builder's add_ingredient call.
builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"})",
"ingredient.c2pa");

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`:

```cpp
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [
{
"action": "c2pa.opened",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
"parameters": { "ingredientIds": ["parent-photo"] }
},
{
"action": "c2pa.placed",
"parameters": { "ingredientIds": ["overlay-graphic"] }
}
]
}
}]
})";

auto builder = c2pa::Builder(context, manifest_json);

builder.add_ingredient(
R"({"title": "photo.jpg", "relationship": "parentOf", "label": "parent-photo"})",
"photo_archive.c2pa");
builder.add_ingredient(
R"({"title": "overlay.png", "relationship": "componentOf", "label": "overlay-graphic"})",
"overlay_archive.c2pa");

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

A single `c2pa.placed` action can also reference several `componentOf` ingredients composited together. List all labels in the `ingredientIds` array:

```cpp
auto manifest_json = R"({
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
"assertions": [{
"label": "c2pa.actions.v2",
"data": {
"actions": [{
"action": "c2pa.placed",
"parameters": {
"ingredientIds": ["base-layer", "overlay-layer"]
}
}]
}
}]
})";

auto builder = c2pa::Builder(context, manifest_json);

builder.add_ingredient(
R"({"title": "base.jpg", "relationship": "componentOf", "label": "base-layer"})",
"base_ingredient.c2pa");
builder.add_ingredient(
R"({"title": "overlay.jpg", "relationship": "componentOf", "label": "overlay-layer"})",
"overlay_ingredient.c2pa");

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

After signing, the action's `parameters.ingredients` array contains one resolved URL per ingredient.

### Ingredient relationships

Specify the relationship between the ingredient and the current asset:
Expand Down
Loading
Loading