From 025e3b36f1b4ac07c3b55dfa85144b0df5560d24 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 19 Feb 2026 20:08:20 -0800 Subject: [PATCH 01/16] fix: Add docs --- docs/selective-manifests.md | 659 ++++++++++++++++++++++++++++++++++++ 1 file changed, 659 insertions(+) create mode 100644 docs/selective-manifests.md diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md new file mode 100644 index 00000000..678ef561 --- /dev/null +++ b/docs/selective-manifests.md @@ -0,0 +1,659 @@ +# Selective manifest construction with Builder and Reader + +This guide explains how to use `Builder` and `Reader` together to selectively construct manifests, keeping only the parts you need and leaving out the rest. This process is best described as **filtering**: you read an existing manifest, choose which elements to retain, and build a new manifest containing only those elements. + +A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains **assertions** (statements about the asset), **ingredients** (references to source assets), and binary resources like **thumbnails**. + +Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information**. Every filtering operation produces a new `Builder` instance. + +> **Important**: Filtering always creates a new `Builder`. The original signed asset and its manifest are never modified. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest from scratch. + +## Core concept + +```mermaid +flowchart LR + A[Signed Asset] -->|Reader| B[JSON + Resources] + B -->|Filter| C[Filtered Data] + C -->|new Builder| D[New Builder] + D -->|sign| E[New Asset] +``` + +The fundamental workflow is: + +1. **Read** the existing manifest with `Reader` to get JSON and binary resources +2. **Filter** the parts you want to keep (parse the JSON, select elements) +3. **Create a new `Builder`** with only the filtered parts +4. **Sign** the new `Builder` into the output asset + +## Reading an existing manifest + +Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The `Reader` does not modify the source asset in any way. + +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image/jpeg", source_stream); + +// Get the full manifest store as JSON +std::string store_json = reader.json(); +auto parsed = json::parse(store_json); + +// Identify the active manifest +std::string active = parsed["active_manifest"]; +auto manifest = parsed["manifests"][active]; + +// Access specific parts +auto ingredients = manifest["ingredients"]; +auto assertions = manifest["assertions"]; +auto thumbnail_id = manifest["thumbnail"]["identifier"]; +``` + +### Extracting binary resources + +The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. The actual binary content must be extracted separately using `get_resource()`: + +```cpp +// Extract a thumbnail to a stream +std::stringstream thumb_stream(std::ios::in | std::ios::out | std::ios::binary); +reader.get_resource(thumbnail_id, thumb_stream); + +// Or extract to a file +reader.get_resource(thumbnail_id, fs::path("thumbnail.jpg")); +``` + +## Filtering into a new Builder + +Each pattern below creates a **new `Builder`** from filtered data. The original asset is untouched. + +When transferring ingredients between a `Reader` and a new `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources; those identifiers must match what you register with `builder.add_resource()`. + +### Pattern 1: Keep only specific ingredients + +```mermaid +flowchart TD + subgraph Original["Original Manifest via Reader"] + IA[Ingredient A] + IB[Ingredient B] + IC[Ingredient C] + end + subgraph New["New Builder"] + NA[Ingredient A] + NC[Ingredient C] + end + subgraph Resources["Binary Resources"] + TA["thumbnail_A"] --> NA + TC["thumbnail_C"] --> NC + MA["manifest_data_A"] --> NA + MC["manifest_data_C"] --> NC + end + IA -- keep --> NA + IB -. skip .-> X((dropped)) + IC -- keep --> NC + style IB fill:#f99,stroke:#c00 + style X fill:#f99,stroke:#c00 +``` + +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image/jpeg", source_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto ingredients = parsed["manifests"][active]["ingredients"]; + +// Filter: keep only ingredients with a specific relationship +json kept_ingredients = json::array(); +for (auto& ingredient : ingredients) { + if (ingredient["relationship"] == "parentOf") { + kept_ingredients.push_back(ingredient); + } +} + +// Create a new Builder with only the kept ingredients +json new_manifest = json::parse(base_manifest_json); +new_manifest["ingredients"] = kept_ingredients; + +c2pa::Builder builder(context, new_manifest.dump()); + +// Transfer binary resources for kept ingredients only +for (auto& ingredient : kept_ingredients) { + // Transfer thumbnail + if (ingredient.contains("thumbnail")) { + std::string thumb_id = ingredient["thumbnail"]["identifier"]; + std::stringstream thumb(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(thumb_id, thumb); + thumb.seekg(0); + builder.add_resource(thumb_id, thumb); + } + // Transfer manifest_data (the ingredient's own C2PA manifest) + if (ingredient.contains("manifest_data")) { + std::string md_id = ingredient["manifest_data"]["identifier"]; + std::stringstream md(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(md_id, md); + md.seekg(0); + builder.add_resource(md_id, md); + } +} + +// Sign the new Builder into an output asset +builder.sign(source_path, output_path, signer); +``` + +### Pattern 2: Keep only specific assertions + +```cpp +auto assertions = parsed["manifests"][active]["assertions"]; + +json kept_assertions = json::array(); +for (auto& assertion : assertions) { + // Keep training-mining assertions, filter out everything else + if (assertion["label"] == "c2pa.training-mining") { + kept_assertions.push_back(assertion); + } +} + +json new_manifest = json::parse(R"({ + "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] +})"); +new_manifest["assertions"] = kept_assertions; + +// Create a new Builder with only the filtered assertions +c2pa::Builder builder(context, new_manifest.dump()); +builder.sign(source_path, output_path, signer); +``` + +### Pattern 3: Start fresh and preserve provenance + +Sometimes you want to discard all existing assertions and ingredients but still maintain a link to the original asset in the provenance chain. This is done by creating a new `Builder` with a clean manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. + +The key insight is that `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data (`manifest_data`) inside the ingredient record. This means: + +- The new manifest has its own, independent set of assertions (which can be empty) +- The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history +- The provenance chain is unbroken: anyone reading the new asset can follow the ingredient link back to the original + +```mermaid +flowchart TD + subgraph Original["Original Signed Asset"] + OA["Assertions: A, B, C"] + OI["Ingredients: X, Y"] + end + subgraph NewBuilder["New Builder"] + NA["Assertions: (empty or new)"] + NI["Ingredient: original.jpg\n(contains full original manifest as binary data)"] + end + Original -->|"add_ingredient(parentOf)"| NI + NI -.->|"validators can trace back"| Original + + style NA fill:#efe,stroke:#090 + style NI fill:#efe,stroke:#090 +``` + +```cpp +// Create a new Builder with a clean definition +c2pa::Builder builder(context); +builder.with_definition(R"({ + "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}], + "assertions": [] +})"); + +// Add the original as an ingredient to preserve provenance chain. +// add_ingredient() stores the original's manifest as binary data inside the ingredient, +// but does NOT copy the original's assertions into this new manifest. +builder.add_ingredient(R"({"title": "original.jpg", "relationship": "parentOf"})", + original_signed_path); +builder.sign(source_path, output_path, signer); +``` + +## Working with archives + +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing you to save, transfer, or resume the work later. + +There are two distinct types of archives to understand: + +### Builder archives vs. ingredient archives + +```mermaid +flowchart TD + subgraph BA["Builder Archive"] + direction TB + B1["Serialized Builder state\n(manifest definition + all resources)"] + B2["Purpose: defer signing\nto another process or machine"] + B3["Not yet signed"] + end + subgraph IA["Ingredient Archive"] + direction TB + I1["Extracted manifest store\nfrom a signed asset"] + I2["Purpose: carry provenance\nof a source asset"] + I3["Contains real signatures\nfrom the original asset"] + end +``` + +| | Builder archive | Ingredient archive | +|------------------------------------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| +| **What it contains** | The full `Builder` state: manifest definition, resources, and ingredients (not yet signed) | The manifest store data from ingredients that were added to a `Builder` | +| **Purpose** | Persist a work-in-progress `Builder` so it can be resumed or signed later | Carry the provenance history of a source asset so it can be embedded as an ingredient in a new manifest | +| **Created by** | `builder.to_archive(stream)` | Extracted from a signed asset's `manifest_data` via `Reader` | +| **Read with** | `Builder::from_archive(stream)` or `builder.with_archive(stream)` | Passed to `builder.add_resource(id, stream)` alongside ingredient JSON | +| **Signed?** | No, not yet signed | Yes, contains valid signatures from the original asset | +| **Requires verification disabled** | Yes, when reading with `Reader` (`verify_after_reading: false`) | No | + +### The ingredients catalog pattern + +A powerful use of archives is building an **ingredients catalog**: a collection of archived ingredients that you can pick and choose from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time you select only the ones you need. + +```mermaid +flowchart TD + subgraph Catalog["Ingredients Catalog (archived)"] + A1["Archive: photos.c2pa\n(ingredients from photo shoot)"] + A2["Archive: graphics.c2pa\n(ingredients from design assets)"] + A3["Archive: audio.c2pa\n(ingredients from audio tracks)"] + end + subgraph Build["Final Builder"] + direction TB + SEL["Pick and choose ingredients\nfrom any archive in the catalog"] + FB["New Builder with\nselected ingredients only"] + end + A1 -->|"select photo_1, photo_3"| SEL + A2 -->|"select logo"| SEL + A3 -. "skip (not needed)" .-> X((not used)) + SEL --> FB + FB -->|sign| OUT[Signed Output Asset] + + style A3 fill:#eee,stroke:#999 + style X fill:#f99,stroke:#c00 +``` + +```cpp +// Read from a catalog of archived ingredients +c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); + +// Open one archive from the catalog +archive_stream.seekg(0); +c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto available_ingredients = parsed["manifests"][active]["ingredients"]; + +// Pick only the ingredients you need +json selected = json::array(); +for (auto& ingredient : available_ingredients) { + if (ingredient["title"] == "photo_1.jpg" || ingredient["title"] == "logo.png") { + selected.push_back(ingredient); + } +} + +// Create a new Builder with selected ingredients +json manifest = json::parse(R"({ + "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] +})"); +manifest["ingredients"] = selected; +c2pa::Builder builder(context, manifest.dump()); + +// Transfer binary resources for selected ingredients +for (auto& ingredient : selected) { + if (ingredient.contains("thumbnail")) { + std::string id = ingredient["thumbnail"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } + if (ingredient.contains("manifest_data")) { + std::string id = ingredient["manifest_data"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } +} + +builder.sign(source_path, output_path, signer); +``` + +### Extracting ingredients from a builder archive + +You can read a builder archive with `Reader` to filter its contents without modifying the original archive. The filtering produces a new `Builder`: + +```mermaid +flowchart TD + B1[Builder\nunsigned] -->|to_archive| AR[Archive\n.c2pa] + AR -->|"Reader(application/c2pa, stream)\nfiltering, not modifying the archive"| JSON[JSON + binary resources] + JSON -->|"filter + create new Builder"| B2[New Builder] + B2 -->|sign| OUT[Output Asset] +``` + +```cpp +// Disable verification (archives contain unsigned working stores) +c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); + +// Read the archive -- this does not modify the archive +archive_stream.seekg(0); +c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto ingredients = parsed["manifests"][active]["ingredients"]; + +// Create a new Builder with extracted ingredients +json new_manifest = json::parse(base_manifest_json); +new_manifest["ingredients"] = ingredients; + +c2pa::Builder builder(context, new_manifest.dump()); + +// Transfer binary resources +for (auto& ingredient : ingredients) { + if (ingredient.contains("thumbnail")) { + std::string id = ingredient["thumbnail"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } + if (ingredient.contains("manifest_data")) { + std::string id = ingredient["manifest_data"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } +} + +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 reuse it by adding ingredients incrementally. Merging is available as a backup when you end up with multiple working stores that need to be consolidated. + +When merging from multiple sources, resource identifier URIs can collide. Rename them with a suffix to avoid conflicts: + +```mermaid +flowchart TD + subgraph WS1["Working Store A"] + IX[Ingredient X\nthumb: T1] + end + subgraph WS2["Working Store B"] + IY[Ingredient Y\nthumb: T1] + end + IX -->|"keep as T1"| NB[New Builder\nmerged] + IY -->|"rename to T1__1"| NB + NB -->|sign| OUT[Signed Output Asset] + style IY fill:#ff9,stroke:#cc0 + + NOTE["Prefer maintaining a single working store.\nMerge only as a fallback."] + style NOTE fill:#ffd,stroke:#cc0,stroke-dasharray: 5 5 +``` + +```cpp +// Track used resource IDs to detect collisions +std::set used_ids; +int suffix_counter = 0; + +json all_ingredients = json::array(); + +for (auto& archive_stream : archives) { + c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); + auto parsed = json::parse(reader.json()); + std::string active = parsed["active_manifest"]; + auto ingredients = parsed["manifests"][active]["ingredients"]; + + for (auto& ingredient : ingredients) { + // Check for thumbnail ID collision + if (ingredient.contains("thumbnail")) { + std::string id = ingredient["thumbnail"]["identifier"]; + if (used_ids.count(id)) { + std::string new_id = id + "__" + std::to_string(++suffix_counter); + ingredient["thumbnail"]["identifier"] = new_id; + id = new_id; + } + used_ids.insert(id); + // Transfer resource with the (possibly renamed) ID + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } + all_ingredients.push_back(ingredient); + } +} + +// Create a single new Builder with all merged ingredients +json manifest = json::parse(R"({ + "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] +})"); +manifest["ingredients"] = all_ingredients; +c2pa::Builder builder(context, manifest.dump()); +builder.sign(source_path, output_path, signer); +``` + +## Controlling manifest embedding + +By default, `sign()` embeds the manifest directly inside the output asset file. This section covers how to change that behavior. + +### Remove the manifest from the asset entirely + +Use `set_no_embed()` so the signed asset contains no embedded manifest. The manifest bytes are returned from `sign()` and can be stored separately (as a sidecar file, on a server, etc.): + +```mermaid +flowchart LR + subgraph Default["Default (embedded)"] + A1[Output Asset] --- A2[Image data + C2PA manifest] + end + + subgraph NoEmbed["With set_no_embed()"] + B1[Output Asset\nimage data only] ~~~ B2[Manifest bytes\nstore as sidecar\nor upload to server] + end +``` + +```cpp +c2pa::Builder builder(context, manifest_json); +builder.set_no_embed(); +builder.set_remote_url("<>"); + +auto manifest_bytes = builder.sign("image/jpeg", source, dest, signer); +// manifest_bytes contains the full manifest store +// Upload manifest_bytes to the remote URL +// The output asset has no embedded manifest +``` + +Reading back: + +```cpp +c2pa::Reader reader(context, "image/jpeg", dest); +reader.is_embedded(); // false +reader.remote_url(); // "<>" +``` + +## Complete workflow diagram + +```mermaid +flowchart TD + subgraph Step1["Step 1: READ"] + SA[Signed Asset] -->|Reader| RD["reader.json() -- full manifest JSON\nreader.get_resource(id, stream) -- binary"] + end + + subgraph Step2["Step 2: FILTER"] + RD --> FI[Parse JSON] + FI --> F1[Pick ingredients to keep] + FI --> F2[Pick assertions to keep] + FI --> F3[Extract resource IDs for kept items] + F1 & F2 & F3 --> FM[Build new manifest JSON\nwith only filtered items] + end + + subgraph Step3["Step 3: BUILD new Builder"] + FM --> BLD["new Builder with context and filtered_json"] + BLD --> AR[".add_resource for each kept binary resource"] + AR --> AI[".add_ingredient to add original as parent"] + AI --> AA[".add_action to record what was done"] + end + + subgraph Step4["Step 4: SIGN"] + AA --> SIGN["builder.sign(source, output, signer)"] + SIGN --> OUT[Output asset with new manifest\ncontaining only filtered content] + end +``` + +## Recording removal actions + +When you filter content, it is good practice to record what was done using C2PA actions. This maintains the provenance chain and documents the edit history: + +```cpp +builder.add_action(R"({ + "action": "c2pa.filtered", + "parameters": { + "name": "Content filter" + }, + "description": "Filtered selected ingredients and assertions" +})"); +``` + +The following action types are defined in [Section 18.14 of the C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) and are relevant to filtering and removal operations: + +| Action | When to use | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `c2pa.filtered` | A filter was applied to the content | +| `c2pa.edited` | General editing that changed the asset content | +| `c2pa.redacted` | An assertion was redacted (removed) from an ingredient's manifest. See [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction) and [Section 18.14.4.7](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) of the spec | + +Note: Redaction in C2PA has a specific meaning -- it is the process of permanently removing assertions from a manifest when an asset is used as an ingredient (see [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction)). The `c2pa.redacted` action records that such a redaction took place. Consult the [full actions table in the specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) for the complete list of defined actions. + +--- + +## Q&A: Builder, Reader, or both? + +This section answers common questions about when to use each API and how they work together. See the [decision tree](#quick-reference-decision-tree) at the end for a visual summary. + +### When should I use a `Reader`? + +**Use a `Reader` when you only need to inspect or extract data without creating a new manifest.** + +- Validating whether an asset has C2PA credentials +- Displaying provenance information to a user +- Extracting thumbnails for display +- Checking trust status and validation results +- Inspecting ingredient chains + +```cpp +c2pa::Reader reader(context, "image.jpg"); +auto json = reader.json(); // inspect the manifest +reader.get_resource(thumb_id, stream); // extract a thumbnail +``` + +The `Reader` is read-only. It never modifies the source asset. + +### When should I use a `Builder`? + +**Use a `Builder` when you are creating a manifest from scratch on an asset that has no existing C2PA data, or when you intentionally want to start with a clean slate.** + +- Signing a brand-new asset for the first time +- Adding C2PA credentials to an unsigned asset +- Creating a manifest where you define all content yourself + +```cpp +c2pa::Builder builder(context, manifest_json); +builder.add_ingredient(ingredient_json, source_path); // add source material +builder.sign(source_path, output_path, signer); +``` + +Every call to the `Builder` constructor or `Builder::from_archive()` creates a new `Builder`. There is no way to modify an existing signed manifest directly. + +### When should I use both `Reader` and `Builder` together? + +**Use both when you need to filter content from an existing manifest into a new one. The `Reader` extracts data, your code filters it, and a new `Builder` receives only the selected parts.** + +- Filtering specific ingredients from a manifest +- Dropping specific assertions while keeping others +- Merging ingredients from multiple signed assets or archives +- Extracting content from an ingredients catalog +- Re-signing with different settings while keeping some original content + +```cpp +// Read existing (does not modify the asset) +c2pa::Reader reader(context, "signed.jpg"); +auto parsed = json::parse(reader.json()); + +// Filter what to keep +auto kept = filter(parsed); // your filtering logic + +// Create a new Builder with only the filtered content +c2pa::Builder builder(context, kept.dump()); +// ... transfer resources ... +builder.sign(source, output, signer); +``` + +### How do I "strip" all C2PA data from an asset? + +**Simply copy the asset without signing it.** If you need to produce a clean file, some formats may retain embedded JUMBF boxes. The safest approach is to re-encode the asset or use format-specific tools to strip metadata. + +Alternatively, if you want to maintain provenance but start fresh: + +```cpp +c2pa::Builder builder(context, minimal_manifest_json); +builder.add_ingredient(R"({"title": "original.jpg", "relationship": "parentOf"})", + original_path); +builder.sign(source, output, signer); +``` + +This creates a new manifest with the original as an ingredient, documenting that the asset came from the original while carrying forward none of its assertions. + +### What is the difference between `add_ingredient()` and injecting ingredient JSON via `with_definition()`? + +| Approach | What it does | When to use | +|----------|-------------|-------------| +| `add_ingredient(json, path)` | Reads the source asset, extracts its manifest store automatically, generates a thumbnail | Adding a signed asset as an ingredient -- the library handles everything | +| Inject via `with_definition()` + `add_resource()` | You provide the ingredient JSON and all binary resources manually | Reconstructing from an archive or merging from multiple readers, where you already have the data extracted | + +### When should I use archives? + +There are two distinct archive concepts: + +**Builder archives (Working stores archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: + +- Signing must happen on a different machine (e.g., an HSM server) +- You want to checkpoint work-in-progress before signing +- You need to transmit a `Builder` state across a network boundary + +**Ingredient archives** contain the manifest store data (`.c2pa` binary) from ingredients that were added to a `Builder`. When a signed asset is added as an ingredient via `add_ingredient()`, the library extracts and stores its manifest store as `manifest_data` within the ingredient record. When the `Builder` is then serialized via `to_archive()`, these ingredient manifest stores are included. Use ingredient archives when: + +- Building an ingredients catalog for pick-and-choose workflows +- Preserving provenance history from source assets +- Transferring ingredient data between `Reader` and `Builder` + +Key consideration for builder archives: `from_archive()` creates a new `Builder` with **default** context settings. If you need specific settings (e.g., thumbnails disabled), use `with_archive()` on a `Builder` that already has the desired context: + +```cpp +// Preserves your context settings +c2pa::Builder builder(my_context); +builder.with_archive(archive_stream); +builder.sign(source, output, signer); +``` + +### Can I modify a manifest in place? + +**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the changes you want and sign it. This is by design -- it ensures the integrity of the provenance chain. + +### What happens to the provenance chain when I rebuild? + +When you create a new manifest, the chain is preserved once you add the original asset as an ingredient. The ingredient carries the original's manifest data, so validators can trace the full history: + +```mermaid +flowchart RL + C[Filtered\nmanifest] -->|ingredient| B[Edited\nmanifest] + B -->|ingredient| A[Original\nmanifest] +``` + +If you **don't** add the original as an ingredient, the chain is broken -- the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). + +### Quick reference decision tree + +```mermaid +flowchart TD + Q1{Need to read an\nexisting manifest?} + Q1 -->|No| USE_B[Use Builder alone\nnew manifest from scratch] + Q1 -->|Yes| Q2{Need to create a\nnew/modified manifest?} + Q2 -->|No| USE_R[Use Reader alone\ninspect/extract only] + Q2 -->|Yes| USE_BR[Use both Reader + Builder] + USE_BR --> Q3{What to keep?} + Q3 -->|Everything| P1["add_ingredient()\nwith original asset"] + Q3 -->|Some parts| P2["Read JSON, filter,\ncreate new Builder"] + Q3 -->|Nothing| P3["New Builder alone\nfresh manifest"] + Q1 -->|Strip all C2PA?| STRIP["Copy asset without signing\nno Reader or Builder needed"] +``` From f475d3e3c1bab1aa6cbb6f85e2aa861d0252e58b Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:20:39 -0800 Subject: [PATCH 02/16] docs: Clarify selective manifest construction process Rephrase and clarify the explanation of using Builder and Reader for selective manifest construction, emphasizing the read-only nature and the importance of filtering. --- docs/selective-manifests.md | 84 +++++++++++-------------------------- 1 file changed, 25 insertions(+), 59 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 678ef561..d444f025 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,12 +1,12 @@ # Selective manifest construction with Builder and Reader -This guide explains how to use `Builder` and `Reader` together to selectively construct manifests, keeping only the parts you need and leaving out the rest. This process is best described as **filtering**: you read an existing manifest, choose which elements to retain, and build a new manifest containing only those elements. +`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This process is best described as **filtering**: read an existing manifest, choose which elements to retain, and build a new manifest containing only those elements. A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains **assertions** (statements about the asset), **ingredients** (references to source assets), and binary resources like **thumbnails**. -Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information**. Every filtering operation produces a new `Builder` instance. +Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information** ("re-build"). THis process produces a new `Builder` instance. -> **Important**: Filtering always creates a new `Builder`. The original signed asset and its manifest are never modified. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest from scratch. +> **Important**: Filtering always creates a new `Builder`. The original signed asset and its manifest are never modified. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest. ## Core concept @@ -21,13 +21,14 @@ flowchart LR The fundamental workflow is: 1. **Read** the existing manifest with `Reader` to get JSON and binary resources -2. **Filter** the parts you want to keep (parse the JSON, select elements) +2. **Filter** the parts to keep (parse the JSON, select elements) 3. **Create a new `Builder`** with only the filtered parts 4. **Sign** the new `Builder` into the output asset ## Reading an existing manifest -Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The `Reader` does not modify the source asset in any way. +Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). +The `Reader` does not modify the source asset in any way. ```cpp c2pa::Context context; @@ -62,36 +63,12 @@ reader.get_resource(thumbnail_id, fs::path("thumbnail.jpg")); ## Filtering into a new Builder -Each pattern below creates a **new `Builder`** from filtered data. The original asset is untouched. +Each pattern below creates a **new `Builder`** from filtered data. The original asset (and its manifest store) remains as is. -When transferring ingredients between a `Reader` and a new `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources; those identifiers must match what you register with `builder.add_resource()`. +When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what you register with `builder.add_resource()`. ### Pattern 1: Keep only specific ingredients -```mermaid -flowchart TD - subgraph Original["Original Manifest via Reader"] - IA[Ingredient A] - IB[Ingredient B] - IC[Ingredient C] - end - subgraph New["New Builder"] - NA[Ingredient A] - NC[Ingredient C] - end - subgraph Resources["Binary Resources"] - TA["thumbnail_A"] --> NA - TC["thumbnail_C"] --> NC - MA["manifest_data_A"] --> NA - MC["manifest_data_C"] --> NC - end - IA -- keep --> NA - IB -. skip .-> X((dropped)) - IC -- keep --> NC - style IB fill:#f99,stroke:#c00 - style X fill:#f99,stroke:#c00 -``` - ```cpp c2pa::Context context; c2pa::Reader reader(context, "image/jpeg", source_stream); @@ -162,9 +139,9 @@ builder.sign(source_path, output_path, signer); ### Pattern 3: Start fresh and preserve provenance -Sometimes you want to discard all existing assertions and ingredients but still maintain a link to the original asset in the provenance chain. This is done by creating a new `Builder` with a clean manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. +Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a clean manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. -The key insight is that `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data (`manifest_data`) inside the ingredient record. This means: +The function `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data (`manifest_data`) inside the ingredient record. This means: - The new manifest has its own, independent set of assertions (which can be empty) - The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history @@ -180,7 +157,7 @@ flowchart TD NA["Assertions: (empty or new)"] NI["Ingredient: original.jpg\n(contains full original manifest as binary data)"] end - Original -->|"add_ingredient(parentOf)"| NI + Original -->|"add_ingredient()"| NI NI -.->|"validators can trace back"| Original style NA fill:#efe,stroke:#090 @@ -205,7 +182,7 @@ builder.sign(source_path, output_path, signer); ## Working with archives -A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing you to save, transfer, or resume the work later. +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. There are two distinct types of archives to understand: @@ -219,6 +196,10 @@ flowchart TD B2["Purpose: defer signing\nto another process or machine"] B3["Not yet signed"] end +``` + +```mermaid +flowchart TD subgraph IA["Ingredient Archive"] direction TB I1["Extracted manifest store\nfrom a signed asset"] @@ -233,12 +214,11 @@ flowchart TD | **Purpose** | Persist a work-in-progress `Builder` so it can be resumed or signed later | Carry the provenance history of a source asset so it can be embedded as an ingredient in a new manifest | | **Created by** | `builder.to_archive(stream)` | Extracted from a signed asset's `manifest_data` via `Reader` | | **Read with** | `Builder::from_archive(stream)` or `builder.with_archive(stream)` | Passed to `builder.add_resource(id, stream)` alongside ingredient JSON | -| **Signed?** | No, not yet signed | Yes, contains valid signatures from the original asset | -| **Requires verification disabled** | Yes, when reading with `Reader` (`verify_after_reading: false`) | No | + | ### The ingredients catalog pattern -A powerful use of archives is building an **ingredients catalog**: a collection of archived ingredients that you can pick and choose from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time you select only the ones you need. +A usage example of archives is building an **ingredients catalog**: a collection of archived ingredients that can be picked and choosen from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time select only the ones you need. ```mermaid flowchart TD @@ -311,7 +291,7 @@ builder.sign(source_path, output_path, signer); ### Extracting ingredients from a builder archive -You can read a builder archive with `Reader` to filter its contents without modifying the original archive. The filtering produces a new `Builder`: +A builder archive can be read with `Reader` to filter its contents without modifying the original archive. The filtering produces a new `Builder`: ```mermaid flowchart TD @@ -322,10 +302,7 @@ flowchart TD ``` ```cpp -// Disable verification (archives contain unsigned working stores) -c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); - -// Read the archive -- this does not modify the archive +// Read the archive. This does not modify the archive archive_stream.seekg(0); c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); auto parsed = json::parse(reader.json()); @@ -361,9 +338,9 @@ 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 reuse it by adding ingredients incrementally. Merging is available as a backup when you end up with multiple working stores that need to be consolidated. +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** as the recommended practice is to maintain a single active working store and reuse it by adding ingredients incrementally. Merging is available as a backup when you end up with multiple working stores that need to be consolidated. -When merging from multiple sources, resource identifier URIs can collide. Rename them with a suffix to avoid conflicts: +When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename them with a suffix to avoid conflicts: ```mermaid flowchart TD @@ -426,7 +403,7 @@ builder.sign(source_path, output_path, signer); ## Controlling manifest embedding -By default, `sign()` embeds the manifest directly inside the output asset file. This section covers how to change that behavior. +By default, `sign()` embeds the manifest directly inside the output asset file. ### Remove the manifest from the asset entirely @@ -493,27 +470,16 @@ flowchart TD ## Recording removal actions -When you filter content, it is good practice to record what was done using C2PA actions. This maintains the provenance chain and documents the edit history: - -```cpp -builder.add_action(R"({ - "action": "c2pa.filtered", - "parameters": { - "name": "Content filter" - }, - "description": "Filtered selected ingredients and assertions" -})"); -``` +When content is filtered, it is good practice to record what was done using C2PA actions. This maintains the provenance chain and documents the edit history. The following action types are defined in [Section 18.14 of the C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) and are relevant to filtering and removal operations: | Action | When to use | |------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `c2pa.filtered` | A filter was applied to the content | | `c2pa.edited` | General editing that changed the asset content | | `c2pa.redacted` | An assertion was redacted (removed) from an ingredient's manifest. See [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction) and [Section 18.14.4.7](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) of the spec | -Note: Redaction in C2PA has a specific meaning -- it is the process of permanently removing assertions from a manifest when an asset is used as an ingredient (see [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction)). The `c2pa.redacted` action records that such a redaction took place. Consult the [full actions table in the specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) for the complete list of defined actions. +Note: Redaction in C2PA has a specific meaning: it is the process of permanently removing assertions from a manifest when an asset is used as an ingredient (see [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction)). The `c2pa.redacted` action records that such a redaction took place. Consult the [full actions table in the specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) for the complete list of defined actions. --- From a5bb5927ba084858a48e9560f644fc425d0c6878 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:24:15 -0800 Subject: [PATCH 03/16] docs: typos Removed section on recording removal actions and related C2PA actions. --- docs/selective-manifests.md | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index d444f025..471ebf95 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -214,7 +214,6 @@ flowchart TD | **Purpose** | Persist a work-in-progress `Builder` so it can be resumed or signed later | Carry the provenance history of a source asset so it can be embedded as an ingredient in a new manifest | | **Created by** | `builder.to_archive(stream)` | Extracted from a signed asset's `manifest_data` via `Reader` | | **Read with** | `Builder::from_archive(stream)` or `builder.with_archive(stream)` | Passed to `builder.add_resource(id, stream)` alongside ingredient JSON | - | ### The ingredients catalog pattern @@ -468,21 +467,6 @@ flowchart TD end ``` -## Recording removal actions - -When content is filtered, it is good practice to record what was done using C2PA actions. This maintains the provenance chain and documents the edit history. - -The following action types are defined in [Section 18.14 of the C2PA Technical Specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) and are relevant to filtering and removal operations: - -| Action | When to use | -|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `c2pa.edited` | General editing that changed the asset content | -| `c2pa.redacted` | An assertion was redacted (removed) from an ingredient's manifest. See [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction) and [Section 18.14.4.7](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) of the spec | - -Note: Redaction in C2PA has a specific meaning: it is the process of permanently removing assertions from a manifest when an asset is used as an ingredient (see [Section 6.8](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_redaction)). The `c2pa.redacted` action records that such a redaction took place. Consult the [full actions table in the specification](https://spec.c2pa.org/specifications/specifications/2.3/specs/C2PA_Specification.html#_actions) for the complete list of defined actions. - ---- - ## Q&A: Builder, Reader, or both? This section answers common questions about when to use each API and how they work together. See the [decision tree](#quick-reference-decision-tree) at the end for a visual summary. @@ -602,8 +586,8 @@ When you create a new manifest, the chain is preserved once you add the original ```mermaid flowchart RL - C[Filtered\nmanifest] -->|ingredient| B[Edited\nmanifest] - B -->|ingredient| A[Original\nmanifest] + C[Filtered \n manifest] -->|ingredient| B[Edited \n manifest] + B -->|ingredient| A[Original \n manifest] ``` If you **don't** add the original as an ingredient, the chain is broken -- the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). @@ -613,13 +597,12 @@ If you **don't** add the original as an ingredient, the chain is broken -- the n ```mermaid flowchart TD Q1{Need to read an\nexisting manifest?} - Q1 -->|No| USE_B[Use Builder alone\nnew manifest from scratch] - Q1 -->|Yes| Q2{Need to create a\nnew/modified manifest?} - Q2 -->|No| USE_R[Use Reader alone\ninspect/extract only] + Q1 -->|No| USE_B[Use Builder alone \n new manifest from scratch] + Q1 -->|Yes| Q2{Need to create a \n new/modified manifest?} + Q2 -->|No| USE_R[Use Reader alone \n inspect/extract only] Q2 -->|Yes| USE_BR[Use both Reader + Builder] USE_BR --> Q3{What to keep?} - Q3 -->|Everything| P1["add_ingredient()\nwith original asset"] - Q3 -->|Some parts| P2["Read JSON, filter,\ncreate new Builder"] - Q3 -->|Nothing| P3["New Builder alone\nfresh manifest"] - Q1 -->|Strip all C2PA?| STRIP["Copy asset without signing\nno Reader or Builder needed"] + Q3 -->|Everything| P1["add_ingredient() \n with original asset"] + Q3 -->|Some parts| P2["Read JSON, filter, \n create new Builder"] + Q3 -->|Nothing| P3["New Builder alone \n fresh manifest"] ``` From e58fee6de854cc96bf2aa87c938c7554d0d23c71 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:27:36 -0800 Subject: [PATCH 04/16] Fix formatting issues in selective-manifests.md --- docs/selective-manifests.md | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 471ebf95..7c064829 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -155,7 +155,7 @@ flowchart TD end subgraph NewBuilder["New Builder"] NA["Assertions: (empty or new)"] - NI["Ingredient: original.jpg\n(contains full original manifest as binary data)"] + NI["Ingredient: original.jpg (contains full original manifest as binary data)"] end Original -->|"add_ingredient()"| NI NI -.->|"validators can trace back"| Original @@ -192,8 +192,8 @@ There are two distinct types of archives to understand: flowchart TD subgraph BA["Builder Archive"] direction TB - B1["Serialized Builder state\n(manifest definition + all resources)"] - B2["Purpose: defer signing\nto another process or machine"] + B1["Serialized Builder state (manifest definition + all resources)"] + B2["Purpose: defer signing to another process or machine"] B3["Not yet signed"] end ``` @@ -202,9 +202,9 @@ flowchart TD flowchart TD subgraph IA["Ingredient Archive"] direction TB - I1["Extracted manifest store\nfrom a signed asset"] - I2["Purpose: carry provenance\nof a source asset"] - I3["Contains real signatures\nfrom the original asset"] + I1["Extracted manifest store from a signed asset"] + I2["Purpose: carry provenance of a source asset"] + I3["Contains real signatures from the original asset"] end ``` @@ -294,8 +294,8 @@ A builder archive can be read with `Reader` to filter its contents without modif ```mermaid flowchart TD - B1[Builder\nunsigned] -->|to_archive| AR[Archive\n.c2pa] - AR -->|"Reader(application/c2pa, stream)\nfiltering, not modifying the archive"| JSON[JSON + binary resources] + B1[Builder unsigned] -->|to_archive| AR[Archive.c2pa] + AR -->|"Reader(application/c2pa, stream) filtering, not modifying the archive"| JSON[JSON + binary resources] JSON -->|"filter + create new Builder"| B2[New Builder] B2 -->|sign| OUT[Output Asset] ``` @@ -354,7 +354,7 @@ flowchart TD NB -->|sign| OUT[Signed Output Asset] style IY fill:#ff9,stroke:#cc0 - NOTE["Prefer maintaining a single working store.\nMerge only as a fallback."] + NOTE["Prefer maintaining a single working store. Merge only as a fallback."] style NOTE fill:#ffd,stroke:#cc0,stroke-dasharray: 5 5 ``` @@ -451,7 +451,7 @@ flowchart TD FI --> F1[Pick ingredients to keep] FI --> F2[Pick assertions to keep] FI --> F3[Extract resource IDs for kept items] - F1 & F2 & F3 --> FM[Build new manifest JSON\nwith only filtered items] + F1 & F2 & F3 --> FM[Build new manifest JSON with only filtered items] end subgraph Step3["Step 3: BUILD new Builder"] @@ -463,7 +463,7 @@ flowchart TD subgraph Step4["Step 4: SIGN"] AA --> SIGN["builder.sign(source, output, signer)"] - SIGN --> OUT[Output asset with new manifest\ncontaining only filtered content] + SIGN --> OUT[Output asset with new manifest containing only filtered content] end ``` @@ -548,8 +548,8 @@ This creates a new manifest with the original as an ingredient, documenting that | Approach | What it does | When to use | |----------|-------------|-------------| -| `add_ingredient(json, path)` | Reads the source asset, extracts its manifest store automatically, generates a thumbnail | Adding a signed asset as an ingredient -- the library handles everything | -| Inject via `with_definition()` + `add_resource()` | You provide the ingredient JSON and all binary resources manually | Reconstructing from an archive or merging from multiple readers, where you already have the data extracted | +| `add_ingredient(json, path)` | Reads the source asset, extracts its manifest store automatically, generates a thumbnail | Adding a signed asset as an ingredient; the library handles everything | +| Inject via `with_definition()` + `add_resource()` | When providing the ingredient JSON and all binary resources manually | Reconstructing from an archive or merging from multiple readers, where you already have the data extracted | ### When should I use archives? @@ -586,8 +586,8 @@ When you create a new manifest, the chain is preserved once you add the original ```mermaid flowchart RL - C[Filtered \n manifest] -->|ingredient| B[Edited \n manifest] - B -->|ingredient| A[Original \n manifest] + C[Filtered manifest] -->|ingredient| B[Edited manifest] + B -->|ingredient| A[Original manifest] ``` If you **don't** add the original as an ingredient, the chain is broken -- the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). @@ -597,12 +597,12 @@ If you **don't** add the original as an ingredient, the chain is broken -- the n ```mermaid flowchart TD Q1{Need to read an\nexisting manifest?} - Q1 -->|No| USE_B[Use Builder alone \n new manifest from scratch] - Q1 -->|Yes| Q2{Need to create a \n new/modified manifest?} - Q2 -->|No| USE_R[Use Reader alone \n inspect/extract only] + Q1 -->|No| USE_B[Use Builder alone new manifest from scratch] + Q1 -->|Yes| Q2{Need to create a new/modified manifest?} + Q2 -->|No| USE_R[Use Reader alone inspect/extract only] Q2 -->|Yes| USE_BR[Use both Reader + Builder] USE_BR --> Q3{What to keep?} - Q3 -->|Everything| P1["add_ingredient() \n with original asset"] - Q3 -->|Some parts| P2["Read JSON, filter, \n create new Builder"] - Q3 -->|Nothing| P3["New Builder alone \n fresh manifest"] + Q3 -->|Everything| P1["add_ingredient() with original asset"] + Q3 -->|Some parts| P2["Read JSON, filter, create new Builder"] + Q3 -->|Nothing| P3["New Builder alone fresh manifest"] ``` From cb7a7d0c208ed56de75a2e037710a21d389fb237 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:29:34 -0800 Subject: [PATCH 05/16] Fix formatting in selective-manifests.md --- docs/selective-manifests.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 7c064829..fb42c313 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -222,14 +222,14 @@ A usage example of archives is building an **ingredients catalog**: a collection ```mermaid flowchart TD subgraph Catalog["Ingredients Catalog (archived)"] - A1["Archive: photos.c2pa\n(ingredients from photo shoot)"] - A2["Archive: graphics.c2pa\n(ingredients from design assets)"] - A3["Archive: audio.c2pa\n(ingredients from audio tracks)"] + A1["Archive: photos.c2pa (ingredients from photo shoot)"] + A2["Archive: graphics.c2pa (ingredients from design assets)"] + A3["Archive: audio.c2pa (ingredients from audio tracks)"] end subgraph Build["Final Builder"] direction TB - SEL["Pick and choose ingredients\nfrom any archive in the catalog"] - FB["New Builder with\nselected ingredients only"] + SEL["Pick and choose ingredients from any archive in the catalog"] + FB["New Builder with selected ingredients only"] end A1 -->|"select photo_1, photo_3"| SEL A2 -->|"select logo"| SEL From a34863bfeeecf7aed64628f333e39f32242e1f1c Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Thu, 19 Feb 2026 20:30:46 -0800 Subject: [PATCH 06/16] Fix formatting in selective manifests diagram Removed newline characters in ingredient labels and updated the New Builder label for clarity. --- docs/selective-manifests.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index fb42c313..a1ed8ff7 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -344,12 +344,12 @@ When merging from multiple sources, resource identifier URIs can collide. One wa ```mermaid flowchart TD subgraph WS1["Working Store A"] - IX[Ingredient X\nthumb: T1] + IX[Ingredient X thumb: T1] end subgraph WS2["Working Store B"] - IY[Ingredient Y\nthumb: T1] + IY[Ingredient Y thumb: T1] end - IX -->|"keep as T1"| NB[New Builder\nmerged] + IX -->|"keep as T1"| NB[New Builder (merged)] IY -->|"rename to T1__1"| NB NB -->|sign| OUT[Signed Output Asset] style IY fill:#ff9,stroke:#cc0 From 25330ab821f4010839d298f8f383b4c9284768ad Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Fri, 20 Feb 2026 18:30:55 -0800 Subject: [PATCH 07/16] Initial copy edit --- docs/selective-manifests.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index a1ed8ff7..a07441ef 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,10 +1,14 @@ # Selective manifest construction with Builder and Reader -`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This process is best described as **filtering**: read an existing manifest, choose which elements to retain, and build a new manifest containing only those elements. +You can use `Builder` and `Reader` together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This process is best described as *filtering*: + +1. Read an existing manifest. +1. Choose which elements to retain. +1. Build a new manifest containing only those elements. A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains **assertions** (statements about the asset), **ingredients** (references to source assets), and binary resources like **thumbnails**. -Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information** ("re-build"). THis process produces a new `Builder` instance. +Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information** ("re-build"). This process produces a new `Builder` instance. > **Important**: Filtering always creates a new `Builder`. The original signed asset and its manifest are never modified. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest. @@ -182,7 +186,7 @@ builder.sign(source_path, output_path, signer); ## Working with archives -A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing you to save, transfer, or resume the work later. There are two distinct types of archives to understand: @@ -217,7 +221,7 @@ flowchart TD ### The ingredients catalog pattern -A usage example of archives is building an **ingredients catalog**: a collection of archived ingredients that can be picked and choosen from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time select only the ones you need. +A usage example of archives is building an **ingredients catalog**: a collection of archived ingredients that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time select only the ones you need. ```mermaid flowchart TD @@ -339,7 +343,7 @@ builder.sign(source_path, output_path, signer); 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** as the recommended practice is to maintain a single active working store and reuse it by adding ingredients incrementally. Merging is available as a backup when you end up with multiple working stores that need to be consolidated. -When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename them with a suffix to avoid conflicts: +When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename identifiers with a unique suffix: ```mermaid flowchart TD From 8adf351d13ed638e64125429439d3f6060fc4e2f Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:18:37 -0800 Subject: [PATCH 08/16] fix: Refine documentation on selective manifest construction --- docs/selective-manifests.md | 61 ++++++++++++++----------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index a07441ef..c7863484 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,16 +1,16 @@ # Selective manifest construction with Builder and Reader -You can use `Builder` and `Reader` together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This process is best described as *filtering*: +`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is usfeul in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). This process is best described as filtering, or rebuilding a working store: 1. Read an existing manifest. 1. Choose which elements to retain. 1. Build a new manifest containing only those elements. -A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains **assertions** (statements about the asset), **ingredients** (references to source assets), and binary resources like **thumbnails**. +A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains assertions (statements about the asset), ingredients (references to other assets used to create the signed asset), and binary resources (like thumbnails). -Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter what is needed, and create a new `Builder` with only that information** ("re-build"). This process produces a new `Builder` instance. +Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter out what is needed, and create a new `Builder` with only that information added back on the new Builder**. This process produces a new `Builder` instance ("rebuild"). -> **Important**: Filtering always creates a new `Builder`. The original signed asset and its manifest are never modified. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest. +> **Important**: This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. ## Core concept @@ -25,8 +25,8 @@ flowchart LR The fundamental workflow is: 1. **Read** the existing manifest with `Reader` to get JSON and binary resources -2. **Filter** the parts to keep (parse the JSON, select elements) -3. **Create a new `Builder`** with only the filtered parts +2. **Identify and filter** the parts to keep (parse the JSON, select and gather elements) +3. **Create a new `Builder`** with only the selected parts based on the applied filtering rules 4. **Sign** the new `Builder` into the output asset ## Reading an existing manifest @@ -42,7 +42,7 @@ c2pa::Reader reader(context, "image/jpeg", source_stream); std::string store_json = reader.json(); auto parsed = json::parse(store_json); -// Identify the active manifest +// Identify the active manifest, which is the current/latest manifest std::string active = parsed["active_manifest"]; auto manifest = parsed["manifests"][active]; @@ -67,11 +67,11 @@ reader.get_resource(thumbnail_id, fs::path("thumbnail.jpg")); ## Filtering into a new Builder -Each pattern below creates a **new `Builder`** from filtered data. The original asset (and its manifest store) remains as is. +Each example below creates a **new `Builder`** from filtered data. The original asset (and its manifest store) remains as is. When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what you register with `builder.add_resource()`. -### Pattern 1: Keep only specific ingredients +### Example 1: Keep only specific ingredients ```cpp c2pa::Context context; @@ -118,7 +118,7 @@ for (auto& ingredient : kept_ingredients) { builder.sign(source_path, output_path, signer); ``` -### Pattern 2: Keep only specific assertions +### Example 2: Keep only specific assertions ```cpp auto assertions = parsed["manifests"][active]["assertions"]; @@ -141,13 +141,13 @@ c2pa::Builder builder(context, new_manifest.dump()); builder.sign(source_path, output_path, signer); ``` -### Pattern 3: Start fresh and preserve provenance +### Example 3: Start fresh and preserve provenance -Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a clean manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. +Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a new manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. -The function `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data (`manifest_data`) inside the ingredient record. This means: +The function `add_ingredient()` does not copy the original's assertions into the new manifest. Instead, it stores the original's entire manifest store as opaque binary data inside the ingredient record. This means: -- The new manifest has its own, independent set of assertions (which can be empty) +- The new manifest has its own, independent set of assertions - The original's full manifest is preserved inside the ingredient, so validators can inspect the full provenance history - The provenance chain is unbroken: anyone reading the new asset can follow the ingredient link back to the original @@ -169,7 +169,7 @@ flowchart TD ``` ```cpp -// Create a new Builder with a clean definition +// Create a new Builder with a new definition c2pa::Builder builder(context); builder.with_definition(R"({ "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}], @@ -186,9 +186,9 @@ builder.sign(source_path, output_path, signer); ## Working with archives -A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing you to save, transfer, or resume the work later. +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. -There are two distinct types of archives to understand: +There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working stores archives) and ingredient archives. ### Builder archives vs. ingredient archives @@ -221,7 +221,7 @@ flowchart TD ### The ingredients catalog pattern -A usage example of archives is building an **ingredients catalog**: a collection of archived ingredients that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients from a different source, and at build time select only the ones you need. +A usage example of ingredient archives is building an **ingredients catalog**: a collection of archived ingredients (with 1 ingredient per archive) that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients, and at build time select only the ones you need. ```mermaid flowchart TD @@ -294,7 +294,7 @@ builder.sign(source_path, output_path, signer); ### Extracting ingredients from a builder archive -A builder archive can be read with `Reader` to filter its contents without modifying the original archive. The filtering produces a new `Builder`: +A Builder archive (or working store archive) can be read with `Reader` to filter its contents without modifying the original archive and use the selected filtered content to create a **new** Builder. This produces a new `Builder`: ```mermaid flowchart TD @@ -341,7 +341,7 @@ 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** as the recommended practice is to maintain a single active working store and reuse it by adding ingredients incrementally. Merging is available as a backup when you end up with multiple working stores that need to be consolidated. +In some cases you may need to merge ingredients from multiple working stores (builder archives) or multiple working stores into a single `Builder`. This should be a **fallback strategy** as the recommended practice is to maintain a **single** active working store and reuse it by adding ingredients incrementally (which is where having archived ingredients catalogs can be helpful). Merging is available as a backup when you end up with multiple working stores that need to be consolidated. When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename identifiers with a unique suffix: @@ -473,7 +473,7 @@ flowchart TD ## Q&A: Builder, Reader, or both? -This section answers common questions about when to use each API and how they work together. See the [decision tree](#quick-reference-decision-tree) at the end for a visual summary. +This section answers questions about when to use each API and how they work together. ### When should I use a `Reader`? @@ -533,21 +533,6 @@ c2pa::Builder builder(context, kept.dump()); builder.sign(source, output, signer); ``` -### How do I "strip" all C2PA data from an asset? - -**Simply copy the asset without signing it.** If you need to produce a clean file, some formats may retain embedded JUMBF boxes. The safest approach is to re-encode the asset or use format-specific tools to strip metadata. - -Alternatively, if you want to maintain provenance but start fresh: - -```cpp -c2pa::Builder builder(context, minimal_manifest_json); -builder.add_ingredient(R"({"title": "original.jpg", "relationship": "parentOf"})", - original_path); -builder.sign(source, output, signer); -``` - -This creates a new manifest with the original as an ingredient, documenting that the asset came from the original while carrying forward none of its assertions. - ### What is the difference between `add_ingredient()` and injecting ingredient JSON via `with_definition()`? | Approach | What it does | When to use | @@ -582,7 +567,7 @@ builder.sign(source, output, signer); ### Can I modify a manifest in place? -**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the changes you want and sign it. This is by design -- it ensures the integrity of the provenance chain. +**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the changes you want and sign it. This is by design: it ensures the integrity of the provenance chain. ### What happens to the provenance chain when I rebuild? @@ -594,7 +579,7 @@ flowchart RL B -->|ingredient| A[Original manifest] ``` -If you **don't** add the original as an ingredient, the chain is broken -- the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). +If you don't add the original as an ingredient, the provenance chain is broken: the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). ### Quick reference decision tree From ab786582eef2db28b9cfb2f2ad3adba996ffd448 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:20:25 -0800 Subject: [PATCH 09/16] Fix label in selective-manifests documentation --- docs/selective-manifests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index c7863484..ec8194da 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -353,7 +353,7 @@ flowchart TD subgraph WS2["Working Store B"] IY[Ingredient Y thumb: T1] end - IX -->|"keep as T1"| NB[New Builder (merged)] + IX -->|"keep as T1"| NB[New Builder] IY -->|"rename to T1__1"| NB NB -->|sign| OUT[Signed Output Asset] style IY fill:#ff9,stroke:#cc0 From 75c0a27d2384b7ab72ba273d2b2f7ceac737a435 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:12:22 -0800 Subject: [PATCH 10/16] Update selective-manifests.md --- docs/selective-manifests.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index ec8194da..4997076c 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -247,7 +247,9 @@ flowchart TD ```cpp // Read from a catalog of archived ingredients -c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); +// verify_after_reading is not needed for newer versions of the SDK +// c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); +c2pa::Context archive_ctx(R"<>"); // Open one archive from the catalog archive_stream.seekg(0); @@ -419,7 +421,7 @@ flowchart LR end subgraph NoEmbed["With set_no_embed()"] - B1[Output Asset\nimage data only] ~~~ B2[Manifest bytes\nstore as sidecar\nor upload to server] + B1[Output Asset] ~~~ B2[Manifest bytes with store as sidecar or uploaded to server] end ``` From 7029c6e6aa4d6cf66ffdffcc41dd4c6d6a792009 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:56:59 -0800 Subject: [PATCH 11/16] fix: More docs (#164) * fix: More docs * Update selective-manifests.md * fix: Typos * fix: Typos * fix: Typos --------- Co-authored-by: Tania Mathern --- docs/selective-manifests.md | 820 +++++++++++++++++++++++++++++++----- tests/builder.test.cpp | 746 ++++++++++++++++++++++++++++++++ 2 files changed, 1454 insertions(+), 112 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 4997076c..3c0f8986 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,6 +1,6 @@ # Selective manifest construction with Builder and Reader -`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is usfeul in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). This process is best described as filtering, or rebuilding a working store: +`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is useful in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). This process is best described as filtering, or rebuilding a working store: 1. Read an existing manifest. 1. Choose which elements to retain. @@ -12,7 +12,7 @@ Since both `Reader` and `Builder` are **read-only by design** (there is no `remo > **Important**: This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. -## Core concept +## Core concepts ```mermaid flowchart LR @@ -69,7 +69,7 @@ reader.get_resource(thumbnail_id, fs::path("thumbnail.jpg")); Each example below creates a **new `Builder`** from filtered data. The original asset (and its manifest store) remains as is. -When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what you register with `builder.add_resource()`. +When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what is registered with `builder.add_resource()`. ### Example 1: Keep only specific ingredients @@ -132,7 +132,7 @@ for (auto& assertion : assertions) { } json new_manifest = json::parse(R"({ - "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}] })"); new_manifest["assertions"] = kept_assertions; @@ -172,7 +172,7 @@ flowchart TD // Create a new Builder with a new definition c2pa::Builder builder(context); builder.with_definition(R"({ - "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}], + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}], "assertions": [] })"); @@ -184,44 +184,255 @@ builder.add_ingredient(R"({"title": "original.jpg", "relationship": "parentOf"}) builder.sign(source_path, output_path, signer); ``` -## Working with archives +## Adding actions to a working store -A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. +Actions record what was done to an asset (e.g. color adjustments, cropping, placing content). Use `builder.add_action()` to add actions to a working store. -There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working stores archives) and ingredient archives. +```cpp +builder.add_action(R"({ + "action": "c2pa.color_adjustments", + "parameters": { "name": "brightnesscontrast" } +})"); -### Builder archives vs. ingredient archives +builder.add_action(R"({ + "action": "c2pa.filtered", + "parameters": { "name": "A filter" }, + "description": "Filtering applied" +})"); +``` -```mermaid -flowchart TD - subgraph BA["Builder Archive"] - direction TB - B1["Serialized Builder state (manifest definition + all resources)"] - B2["Purpose: defer signing to another process or machine"] - B3["Not yet signed"] - end +### Action JSON fields + +| Field | Required | Description | +|-------|----------|-------------| +| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | +| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | +| `description` | No | Human-readable description of what happened | +| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | + +### Linking actions to ingredients + +When an action involves a specific ingredient, the ingredient and action need to be linked. This is done using `ingredientIds` in the action's parameters, which references a matching key on the ingredient. + +#### How `ingredientIds` resolution works + +The SDK matches each value in `ingredientIds` against ingredients using this priority: + +1. **`label`** on the ingredient (primary): if set and non-empty, this is used as the linking key +2. **`instance_id`** on the ingredient (fallback): used when `label` is absent or empty + +#### Linking with `label` + +The `label` field on an ingredient is the **primary** linking key. Set a `label` on the ingredient and reference it in the action's `ingredientIds`. The label can be any string: it acts as a linking key between the ingredient and the action. + +```cpp +c2pa::Context context; + +auto manifest_json = R"( +{ + "claim_generator_info": [{ "name": "an-application", "version": "1.0" }], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation" + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3"] + } + } + ] + } + } + ] +} +)"; + +c2pa::Builder builder(context, manifest_json); + +// The label on the ingredient matches the value in ingredientIds +auto ingredient_json = R"( +{ + "title": "photo.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3" +} +)"; +builder.add_ingredient(ingredient_json, photo_path); + +builder.sign(source_path, output_path, signer); ``` -```mermaid -flowchart TD - subgraph IA["Ingredient Archive"] - direction TB - I1["Extracted manifest store from a signed asset"] - I2["Purpose: carry provenance of a source asset"] - I3["Contains real signatures from the original asset"] - end +##### Multiple ingredients need distinct identifying labels + +When linking multiple ingredients, each ingredient needs a unique label: + +```cpp +auto manifest_json = R"( +{ + "claim_generator_info": [{ "name": "an-application", "version": "1.0" }], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_1"] + } + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_2"] + } + } + ] + } + } + ] +} +)"; + +c2pa::Builder builder(context, manifest_json); + +// parentOf ingredient linked to c2pa.opened +builder.add_ingredient(R"({ + "title": "original.jpg", + "format": "image/jpeg", + "relationship": "parentOf", + "label": "c2pa.ingredient.v3_1" +})", original_path); + +// componentOf ingredient linked to c2pa.placed +builder.add_ingredient(R"({ + "title": "overlay.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3_2" +})", overlay_path); + +builder.sign(source_path, output_path, signer); ``` -| | Builder archive | Ingredient archive | -|------------------------------------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| -| **What it contains** | The full `Builder` state: manifest definition, resources, and ingredients (not yet signed) | The manifest store data from ingredients that were added to a `Builder` | -| **Purpose** | Persist a work-in-progress `Builder` so it can be resumed or signed later | Carry the provenance history of a source asset so it can be embedded as an ingredient in a new manifest | -| **Created by** | `builder.to_archive(stream)` | Extracted from a signed asset's `manifest_data` via `Reader` | -| **Read with** | `Builder::from_archive(stream)` or `builder.with_archive(stream)` | Passed to `builder.add_resource(id, stream)` alongside ingredient JSON | +> **Note:** The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. + +#### Linking with `instance_id` (fallback) + +When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. + +```cpp +c2pa::Context context; + +// instance_id is used as identified for the ingredient to link, and needs to be unique +std::string instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"; + +json manifest_json = { + {"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.opened"}, + {"parameters", { + {"ingredientIds", json::array({instance_id})} + }} + } + })} + }} + } + })} +}; + +c2pa::Builder builder(context, manifest_json.dump()); + +// No label set: instance_id is used as the linking key +json ingredient = { + {"title", "source_photo.jpg"}, + {"relationship", "parentOf"}, + {"instance_id", instance_id} +}; +builder.add_ingredient(ingredient.dump(), source_photo_path); + +builder.sign(source_path, output_path, signer); +``` + +> Note: The `instance_id` can be read back from the ingredient JSON after signing. + +#### After signing: reading linked ingredients back + +After signing, `ingredientIds` is gone. The action's `parameters.ingredients[]` contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL: + +```cpp +auto reader = c2pa::Reader(context, signed_path); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto manifest = parsed["manifests"][active]; + +// Build a map: label -> ingredient +std::map label_to_ingredient; +for (auto& ing : manifest["ingredients"]) { + label_to_ingredient[ing["label"]] = ing; +} + +// Match each action to its ingredients by extracting labels from URLs +for (auto& assertion : manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action.contains("parameters") && + action["parameters"].contains("ingredients")) { + for (auto& ref : action["parameters"]["ingredients"]) { + std::string url = ref["url"]; + std::string label = url.substr(url.rfind('/') + 1); + auto& matched = label_to_ingredient[label]; + // Now the ingredient is available + } + } + } + } +} +``` + +#### `label` vs `instance_id`: when to use which? + +| Property | `label` | `instance_id` | +| -------- | ------- | ------------- | +| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | +| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | +| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | +| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | + +**Use `label`** when defining manifests in JSON. +**Use `instance_id`** when working programmatically with ingredients whose identity comes from other sources, or when a stable identifier that persists unchanged across rebuilds is needed. + +## Working with archives + +A `Builder` represents a **working store**: a manifest that is being assembled but has not yet been signed. Archives serialize this working store (definition + resources) to a `.c2pa` binary format, allowing to save, transfer, or resume the work later. For more background on working stores and archives, see [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores). + +There are two distinct types of archives, sharing the same binary format but being conceptually different: builder archives (working store archives) and ingredient archives. + +### Builder archives vs. ingredient archives + +A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder::from_archive()` or `builder.with_archive()`. + +An **ingredient archive** contains the manifest store data from an asset that was added as an ingredient. + +The key difference: a builder archive is a work-in-progress (unsigned). An ingredient archive carries the provenance history of a source asset to reuse as ingredient in other working stores. ### The ingredients catalog pattern -A usage example of ingredient archives is building an **ingredients catalog**: a collection of archived ingredients (with 1 ingredient per archive) that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients, and at build time select only the ones you need. +A usage example of ingredient archives is building an **ingredients catalog**: a collection of archived ingredients (with 1 ingredient per archive) that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients, and at build time the caller selects only the ones needed. ```mermaid flowchart TD @@ -249,7 +460,7 @@ flowchart TD // Read from a catalog of archived ingredients // verify_after_reading is not needed for newer versions of the SDK // c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); -c2pa::Context archive_ctx(R"<>"); +c2pa::Context archive_ctx(R"<>"); // Open one archive from the catalog archive_stream.seekg(0); @@ -258,7 +469,7 @@ auto parsed = json::parse(reader.json()); std::string active = parsed["active_manifest"]; auto available_ingredients = parsed["manifests"][active]["ingredients"]; -// Pick only the ingredients you need +// Pick only the needed ingredients json selected = json::array(); for (auto& ingredient : available_ingredients) { if (ingredient["title"] == "photo_1.jpg" || ingredient["title"] == "logo.png") { @@ -268,7 +479,7 @@ for (auto& ingredient : available_ingredients) { // Create a new Builder with selected ingredients json manifest = json::parse(R"({ - "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}] })"); manifest["ingredients"] = selected; c2pa::Builder builder(context, manifest.dump()); @@ -294,76 +505,177 @@ for (auto& ingredient : selected) { builder.sign(source_path, output_path, signer); ``` -### Extracting ingredients from a builder archive +### Overriding ingredient properties when adding from an archive + +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: + +```cpp +// Override title, relationship, and set a custom instance_id for tracking +json ingredient_override = { + {"title", "my-custom-title.jpg"}, + {"relationship", "parentOf"}, + {"instance_id", "my-tracking-id:asset-example-id"} +}; +builder.add_ingredient(ingredient_override.dump(), signed_asset_path); +``` + +The `title`, `relationship`, and `instance_id` fields in the provided JSON take priority. The library fills in everything else (thumbnail, manifest_data, format) automatically from the source asset. This works with any source: a signed asset, a `.c2pa` archive, or an unsigned file. + +### Using custom vendor parameters in actions + +The C2PA specification allows **vendor-namespaced parameters** on actions using reverse domain notation. These custom parameters survive signing and can be read back, making them useful for tagging actions with IDs that help with filtering. + +```cpp +auto manifest_json = R"( +{ + "claim_generator_info": [{ "name": "an-application", "version": "1.0" }], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://c2pa.org/digitalsourcetype/compositeCapture", + "parameters": { + "com.mycompany.tool": "my-editor", + "com.mycompany.session_id": "session-abc-123" + } + }, + { + "action": "c2pa.placed", + "description": "Placed an image", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredientIds": ["c2pa.ingredient.v3"] + } + } + ] + } + } + ] +} +)"; +``` + +After signing, these custom parameters appear alongside the standard fields: + +```json +{ + "action": "c2pa.placed", + "parameters": { + "com.mycompany.layer_id": "layer-42", + "ingredients": [{"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"}] + } +} +``` + +Custom vendor parameters can be used to filter actions. For example, to find all actions related to a specific layer: + +```cpp +for (auto& action : actions) { + if (action.contains("parameters") && + action["parameters"].contains("com.mycompany.layer_id") && + action["parameters"]["com.mycompany.layer_id"] == "layer-42") { + // This action is related to layer-42 + } +} +``` + +> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components following the pattern `[a-zA-Z0-9][a-zA-Z0-9_-]*` (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (eg. `c2pa` or `cawg`) may be reserved. -A Builder archive (or working store archive) can be read with `Reader` to filter its contents without modifying the original archive and use the selected filtered content to create a **new** Builder. This produces a new `Builder`: +### Extracting ingredients from a working store into archives + +An example workflow is to build up a working store with multiple ingredients, archive it, and then later extract specific ingredients from that archive to use in a new working store. ```mermaid flowchart TD - B1[Builder unsigned] -->|to_archive| AR[Archive.c2pa] - AR -->|"Reader(application/c2pa, stream) filtering, not modifying the archive"| JSON[JSON + binary resources] - JSON -->|"filter + create new Builder"| B2[New Builder] - B2 -->|sign| OUT[Output Asset] + subgraph Step1["Step 1: Build a working store with ingredients"] + IA["add_ingredient(A.jpg)"] --> B1[Builder] + IB["add_ingredient(B.jpg)"] --> B1 + B1 -->|"to_archive()"| AR["archive.c2pa"] + end + subgraph Step2["Step 2: Extract ingredients from archive"] + AR -->|"Reader(application/c2pa)"| RD[JSON + resources] + RD -->|"pick ingredients"| SEL[Selected ingredients] + end + subgraph Step3["Step 3: Reuse in a new Builder"] + SEL -->|"new Builder + add_resource()"| B2[New Builder] + B2 -->|sign| OUT[Signed Output] + end +``` + +**Step 1:** Build a working store and archive it: + +```cpp +c2pa::Context context; +c2pa::Builder builder(context, manifest_json); + +// Add ingredients to the working store +builder.add_ingredient(R"({"title": "A.jpg", "relationship": "componentOf"})", + path_to_A); +builder.add_ingredient(R"({"title": "B.jpg", "relationship": "componentOf"})", + path_to_B); + +// Save the working store as an archive +std::stringstream archive_stream(std::ios::in | std::ios::out | std::ios::binary); +builder.to_archive(archive_stream); ``` +**Step 2:** Read the archive and extract ingredients: + ```cpp -// Read the archive. This does not modify the archive +// Read the archive (does not modify it) archive_stream.seekg(0); -c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); +c2pa::Reader reader(context, "application/c2pa", archive_stream); auto parsed = json::parse(reader.json()); std::string active = parsed["active_manifest"]; auto ingredients = parsed["manifests"][active]["ingredients"]; +``` -// Create a new Builder with extracted ingredients -json new_manifest = json::parse(base_manifest_json); -new_manifest["ingredients"] = ingredients; - -c2pa::Builder builder(context, new_manifest.dump()); +**Step 3:** Create a new Builder with the extracted ingredients: -// Transfer binary resources +```cpp +// Pick the desired ingredients +json selected = json::array(); for (auto& ingredient : ingredients) { + if (ingredient["title"] == "A.jpg") { + selected.push_back(ingredient); + } +} + +// Create a new Builder with only the selected ingredients +json new_manifest = json::parse(base_manifest_json); +new_manifest["ingredients"] = selected; +c2pa::Builder new_builder(context, new_manifest.dump()); + +// Transfer binary resources for the selected ingredients +for (auto& ingredient : selected) { if (ingredient.contains("thumbnail")) { std::string id = ingredient["thumbnail"]["identifier"]; std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); reader.get_resource(id, stream); stream.seekg(0); - builder.add_resource(id, stream); + new_builder.add_resource(id, stream); } if (ingredient.contains("manifest_data")) { std::string id = ingredient["manifest_data"]["identifier"]; std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); reader.get_resource(id, stream); stream.seekg(0); - builder.add_resource(id, stream); + new_builder.add_resource(id, stream); } } -builder.sign(source_path, output_path, signer); +new_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) or multiple working stores into a single `Builder`. This should be a **fallback strategy** as the recommended practice is to maintain a **single** active working store and reuse it by adding ingredients incrementally (which is where having archived ingredients catalogs can be helpful). Merging is available as a backup when you end up with multiple working stores that need to be consolidated. +In some cases it may be necessary to merge ingredients from multiple working stores (builder archives) or multiple working stores into a single `Builder`. This should be a **fallback strategy** as the recommended practice is to maintain a **single** active working store and reuse it by adding ingredients incrementally (which is where having archived ingredients catalogs can be helpful). Merging is available as a backup when multiple working stores need to be consolidated. When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename identifiers with a unique suffix: -```mermaid -flowchart TD - subgraph WS1["Working Store A"] - IX[Ingredient X thumb: T1] - end - subgraph WS2["Working Store B"] - IY[Ingredient Y thumb: T1] - end - IX -->|"keep as T1"| NB[New Builder] - IY -->|"rename to T1__1"| NB - NB -->|sign| OUT[Signed Output Asset] - style IY fill:#ff9,stroke:#cc0 - - NOTE["Prefer maintaining a single working store. Merge only as a fallback."] - style NOTE fill:#ffd,stroke:#cc0,stroke-dasharray: 5 5 -``` - ```cpp // Track used resource IDs to detect collisions std::set used_ids; @@ -399,13 +711,303 @@ for (auto& archive_stream : archives) { // Create a single new Builder with all merged ingredients json manifest = json::parse(R"({ - "claim_generator_info": [{"name": "c2pa-cpp-docs", "version": "0.1.0"}] + "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}] })"); manifest["ingredients"] = all_ingredients; c2pa::Builder builder(context, manifest.dump()); builder.sign(source_path, output_path, signer); ``` +## Retrieving actions from a working store + +Use `Reader` to read actions back from an asset, or an archived Builder. Actions are stored in the `c2pa.actions.v2` assertion inside the manifest. + +### Reading actions + +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image/jpeg", source_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto assertions = parsed["manifests"][active]["assertions"]; + +// Find the actions assertion +for (auto& assertion : assertions) { + if (assertion["label"] == "c2pa.actions.v2") { + auto actions = assertion["data"]["actions"]; + for (auto& action : actions) { + std::cout << "Action: " << action["action"] << std::endl; + if (action.contains("description")) { + std::cout << " Description: " << action["description"] << std::endl; + } + } + } +} +``` + +### Reading actions from an archive + +The same approach works for archived Builders. The format parameter `"application/c2pa"` tells the `Reader` to read from an archive stream instead of an asset: + +```cpp +c2pa::Context context; + +// Read from an archive instead of an asset +std::ifstream archive_file("builder_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 assertions = parsed["manifests"][active]["assertions"]; + +// Find the actions assertion -- same as reading from an asset +for (auto& assertion : assertions) { + if (assertion["label"] == "c2pa.actions.v2") { + auto actions = assertion["data"]["actions"]; + for (auto& action : actions) { + std::cout << "Action: " << action["action"] << std::endl; + if (action.contains("description")) { + std::cout << " Description: " << action["description"] << std::endl; + } + } + } +} +``` + +### Understanding the manifest tree + +The `Reader` returns a manifest store, which is a dictionary of manifests. But conceptually, it represents a tree (graph without cycles): each manifest may have ingredients and assertions, and each ingredient can carry its own manifest store (in its `manifest_data` field), which in turn can have its own ingredients, actions, assertions, and so on recursively. + +```mermaid +flowchart TD + subgraph Store["Manifest Store"] + M1["Active Manifest\n- assertions (including c2pa.actions.v2), ingredients"] + M2["Ingredient A's manifest with its own c2pa.actions.v2 and its own ingredients"] + M3["Ingredient B's manifest with its own c2pa.actions.v2"] + end + M1 -->|"ingredient A has manifest_data"| M2 + M1 -->|"ingredient B has manifest_data"| M3 + M2 -->|"may have its own ingredients..."| M4["...deeper in the tree"] +``` + +The `reader.json()` returns all manifests in a flattened `"manifests"` dictionary, keyed by their label (a URN like `contentauth:urn:uuid:...`). The `"active_manifest"` key indicates which one is the top of the tree. + +Each manifest in the tree has its assertions and ingredients. Walking the tree reveals the full provenance chain: what each actor did at each step, including what actions were performed and what ingredients were used. + +**To walk the tree and find actions at each level:** + +```cpp +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto active_manifest = parsed["manifests"][active]; + +// Read the active manifest's actions +for (auto& assertion : active_manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + std::cout << "Active manifest actions:" << std::endl; + for (auto& action : assertion["data"]["actions"]) { + std::cout << " " << action["action"].get() << std::endl; + } + } +} + +// Walk into each ingredient's manifest +for (auto& ingredient : active_manifest["ingredients"]) { + std::cout << "Ingredient: " << ingredient["title"].get() << std::endl; + + // If this ingredient has its own manifest (it was a signed asset), + // its manifest label is in "active_manifest" + if (ingredient.contains("active_manifest")) { + std::string ing_manifest_label = ingredient["active_manifest"]; + if (parsed["manifests"].contains(ing_manifest_label)) { + auto ing_manifest = parsed["manifests"][ing_manifest_label]; + + // This ingredient's manifest has its own actions + for (auto& assertion : ing_manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + std::cout << " Ingredient's actions:" << std::endl; + for (auto& action : assertion["data"]["actions"]) { + std::cout << " " << action["action"].get() << std::endl; + } + } + } + + // And its own ingredients (deeper in the tree)... + } + } else { + // This ingredient has no manifest of its own (it was an unsigned asset). + // It still has a title, format, and relationship, but no manifest_data, + // no actions, and no deeper provenance chain. + std::cout << " (no content credentials)" << std::endl; + } +} +``` + +## Filtering actions (keeping some, removing others) + +Since there is no `remove()` method, the way to remove actions is to **read, pick the ones to keep, create a new Builder**. + +```mermaid +flowchart TD + SA["Signed Asset with 3 actions: opened, placed, filtered"] -->|Reader| JSON[Parse JSON] + JSON -->|"Keep only opened + placed"| FILT[Filtered actions] + FILT -->|"New Builder with 2 actions"| NB[New Builder] + NB -->|sign| OUT["New with 2 actions only: opened, placed"] +``` + +### Basic action filtering + +When filtering, remember that the first action must remain `c2pa.created` or `c2pa.opened` for the manifest to be valid. If the first action is removed, a new one must be added. + +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image/jpeg", source_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto manifest = parsed["manifests"][active]; + +// Filter actions: keep c2pa.created/c2pa.opened (mandatory) and c2pa.placed, drop the rest +json kept_actions = json::array(); +for (auto& assertion : manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + std::string action_type = action["action"]; + if (action_type == "c2pa.created" || action_type == "c2pa.opened" || + action_type == "c2pa.placed") { + kept_actions.push_back(action); + } + // Skip c2pa.filtered, c2pa.color_adjustments, etc. + } + } +} + +// Build a new manifest with only the kept actions +json new_manifest = json::parse(R"({ + "claim_generator_info": [{"name": "an-application", "version": "1.0"}] +})"); + +if (!kept_actions.empty()) { + new_manifest["assertions"] = json::array({ + { + {"label", "c2pa.actions"}, + {"data", {{"actions", kept_actions}}} + } + }); +} + +c2pa::Builder builder(context, new_manifest.dump()); +builder.sign(source_path, output_path, signer); +``` + +### Filtering actions that reference ingredients + +Some actions reference ingredients (via `parameters.ingredients[].url` after signing). If keeping an action that references an ingredient, **the corresponding ingredient and its binary resources must also be kept**. If an ingredient is dropped, any actions that reference it must also be dropped (or updated). + +#### Understanding the `c2pa.opened` action + +The `c2pa.opened` action is special because it must be the first action and it references the asset that was opened (the `parentOf` ingredient). When filtering: + +- **Always keep `c2pa.opened` or `c2pa.created`**: it is required for a valid manifest +- **Keep the ingredient it references**: the `parentOf` ingredient linked via its `parameters.ingredients[].url` +- Removing the ingredient that `c2pa.opened` points to will make the manifest invalid + +#### Understanding the `c2pa.placed` action + +The `c2pa.placed` action references a `componentOf` ingredient that was composited into the asset. When filtering: + +- If keeping `c2pa.placed`, keep the ingredient it references +- If the ingredient is dropped, also drop the `c2pa.placed` action +- If `c2pa.placed` is not required: it can safely be removed (and the ingredient it references, if it is the only reference) + +#### Full example: filtering with linked ingredients + +```cpp +c2pa::Context context; +c2pa::Reader reader(context, "image/jpeg", source_stream); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto manifest = parsed["manifests"][active]; + +// Filter actions and track which ingredients are needed +json kept_actions = json::array(); +std::set needed_ingredient_labels; + +for (auto& assertion : manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + std::string action_type = action["action"]; + + // Always keep c2pa.opened/c2pa.created (required for valid manifest) + // Keep c2pa.placed (optional -- kept here as an example) + // Drop everything else + bool keep = (action_type == "c2pa.opened" || + action_type == "c2pa.created" || + action_type == "c2pa.placed"); + + if (keep) { + kept_actions.push_back(action); + + // Track which ingredients this action needs + if (action.contains("parameters") && + action["parameters"].contains("ingredients")) { + for (auto& ing_ref : action["parameters"]["ingredients"]) { + std::string url = ing_ref["url"]; + std::string label = url.substr(url.rfind('/') + 1); + needed_ingredient_labels.insert(label); + } + } + } + } + } +} + +// Keep only the ingredients that are referenced by kept actions +json kept_ingredients = json::array(); +for (auto& ingredient : manifest["ingredients"]) { + if (ingredient.contains("label") && + needed_ingredient_labels.count(ingredient["label"])) { + kept_ingredients.push_back(ingredient); + } +} + +// Build the new manifest with filtered actions and matching ingredients +json new_manifest = json::parse(R"({ + "claim_generator_info": [{"name": "an-application", "version": "1.0"}] +})"); +new_manifest["ingredients"] = kept_ingredients; +if (!kept_actions.empty()) { + new_manifest["assertions"] = json::array({ + { + {"label", "c2pa.actions"}, + {"data", {{"actions", kept_actions}}} + } + }); +} + +c2pa::Builder builder(context, new_manifest.dump()); + +// Transfer binary resources for kept ingredients +for (auto& ingredient : kept_ingredients) { + if (ingredient.contains("thumbnail")) { + std::string id = ingredient["thumbnail"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } + if (ingredient.contains("manifest_data")) { + std::string id = ingredient["manifest_data"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + builder.add_resource(id, stream); + } +} + +builder.sign(source_path, output_path, signer); +``` + +> **Note:** When copying ingredient JSON objects from a reader, they keep their `label` field. Since the action URLs reference ingredients by label, the links resolve correctly as long as ingredients are not renamed or reindexed. If ingredients are re-added via `add_ingredient()` (which generates new labels), the action URLs will also need to be updated. + ## Controlling manifest embedding By default, `sign()` embeds the manifest directly inside the output asset file. @@ -455,16 +1057,17 @@ flowchart TD subgraph Step2["Step 2: FILTER"] RD --> FI[Parse JSON] FI --> F1[Pick ingredients to keep] - FI --> F2[Pick assertions to keep] - FI --> F3[Extract resource IDs for kept items] - F1 & F2 & F3 --> FM[Build new manifest JSON with only filtered items] + FI --> F3[Pick actions to keep] + FI --> F4[Ensure kept actions' ingredients are also kept] + FI --> F5["Ensure c2pa.created/opened is still the first action"] + F1 & F3 & F4 & F5 --> FM[Build new manifest JSON with only filtered items] end subgraph Step3["Step 3: BUILD new Builder"] FM --> BLD["new Builder with context and filtered_json"] BLD --> AR[".add_resource for each kept binary resource"] - AR --> AI[".add_ingredient to add original as parent"] - AI --> AA[".add_action to record what was done"] + AR --> AI[".add_ingredient to add original as parent (optional)"] + AI --> AA[".add_action to record new actions (optional)"] end subgraph Step4["Step 4: SIGN"] @@ -477,9 +1080,9 @@ flowchart TD This section answers questions about when to use each API and how they work together. -### When should I use a `Reader`? +### When to use a `Reader` -**Use a `Reader` when you only need to inspect or extract data without creating a new manifest.** +**Use a `Reader` when the goal is only to inspect or extract data without creating a new manifest.** - Validating whether an asset has C2PA credentials - Displaying provenance information to a user @@ -495,13 +1098,13 @@ reader.get_resource(thumb_id, stream); // extract a thumbnail The `Reader` is read-only. It never modifies the source asset. -### When should I use a `Builder`? +### When to use a `Builder` -**Use a `Builder` when you are creating a manifest from scratch on an asset that has no existing C2PA data, or when you intentionally want to start with a clean slate.** +**Use a `Builder` when creating a manifest from scratch on an asset that has no existing C2PA data, or when intentionally starting with a clean slate.** - Signing a brand-new asset for the first time - Adding C2PA credentials to an unsigned asset -- Creating a manifest where you define all content yourself +- Creating a manifest with all content defined from scratch ```cpp c2pa::Builder builder(context, manifest_json); @@ -511,12 +1114,13 @@ builder.sign(source_path, output_path, signer); Every call to the `Builder` constructor or `Builder::from_archive()` creates a new `Builder`. There is no way to modify an existing signed manifest directly. -### When should I use both `Reader` and `Builder` together? +### When to use both `Reader` and `Builder` together -**Use both when you need to filter content from an existing manifest into a new one. The `Reader` extracts data, your code filters it, and a new `Builder` receives only the selected parts.** +**Use both when filtering content from an existing manifest into a new one. The `Reader` extracts data, application code filters it, and a new `Builder` receives only the selected parts.** - Filtering specific ingredients from a manifest - Dropping specific assertions while keeping others +- Filtering actions (keeping some, removing others) - Merging ingredients from multiple signed assets or archives - Extracting content from an ingredients catalog - Re-signing with different settings while keeping some original content @@ -527,7 +1131,7 @@ c2pa::Reader reader(context, "signed.jpg"); auto parsed = json::parse(reader.json()); // Filter what to keep -auto kept = filter(parsed); // your filtering logic +auto kept = filter(parsed); // application-specific filtering logic // Create a new Builder with only the filtered content c2pa::Builder builder(context, kept.dump()); @@ -539,18 +1143,18 @@ builder.sign(source, output, signer); | Approach | What it does | When to use | |----------|-------------|-------------| -| `add_ingredient(json, path)` | Reads the source asset, extracts its manifest store automatically, generates a thumbnail | Adding a signed asset as an ingredient; the library handles everything | -| Inject via `with_definition()` + `add_resource()` | When providing the ingredient JSON and all binary resources manually | Reconstructing from an archive or merging from multiple readers, where you already have the data extracted | +| `add_ingredient(json, path)` | Reads the source (a signed asset, an unsigned file, or a `.c2pa` archive), extracts its manifest store automatically, generates a thumbnail | Adding an ingredient where the library should handle extraction. Works with ingredient catalog archives too: pass the archive path and the library extracts the manifest data | +| Inject via `with_definition()` + `add_resource()` | Accepts the ingredient JSON and all binary resources provided manually | Reconstructing from a reader or merging from multiple readers, where the data has already been extracted | -### When should I use archives? +### When to use archives -There are two distinct archive concepts: +There are two distinct archive concepts (see also [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores)): -**Builder archives (Working stores archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: +**Builder archives (Working store archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: - Signing must happen on a different machine (e.g., an HSM server) -- You want to checkpoint work-in-progress before signing -- You need to transmit a `Builder` state across a network boundary +- Checkpointing work-in-progress before signing +- Transmitting a `Builder` state across a network boundary **Ingredient archives** contain the manifest store data (`.c2pa` binary) from ingredients that were added to a `Builder`. When a signed asset is added as an ingredient via `add_ingredient()`, the library extracts and stores its manifest store as `manifest_data` within the ingredient record. When the `Builder` is then serialized via `to_archive()`, these ingredient manifest stores are included. Use ingredient archives when: @@ -558,42 +1162,34 @@ There are two distinct archive concepts: - Preserving provenance history from source assets - Transferring ingredient data between `Reader` and `Builder` -Key consideration for builder archives: `from_archive()` creates a new `Builder` with **default** context settings. If you need specific settings (e.g., thumbnails disabled), use `with_archive()` on a `Builder` that already has the desired context: +Key consideration for builder archives: `from_archive()` creates a new `Builder` with **default** context settings. If specific settings are needed (e.g., thumbnails disabled), use `with_archive()` on a `Builder` that already has the desired context: ```cpp -// Preserves your context settings +// Preserves the caller's context settings c2pa::Builder builder(my_context); builder.with_archive(archive_stream); builder.sign(source, output, signer); ``` -### Can I modify a manifest in place? +### Can a manifest be modified in place? -**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the changes you want and sign it. This is by design: it ensures the integrity of the provenance chain. +**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the desired changes and sign it. This is by design: it ensures the integrity of the provenance chain. -### What happens to the provenance chain when I rebuild? - -When you create a new manifest, the chain is preserved once you add the original asset as an ingredient. The ingredient carries the original's manifest data, so validators can trace the full history: - -```mermaid -flowchart RL - C[Filtered manifest] -->|ingredient| B[Edited manifest] - B -->|ingredient| A[Original manifest] -``` +### What happens to the provenance chain when rebuilding a working store? -If you don't add the original as an ingredient, the provenance chain is broken: the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). +When creating a new manifest, the chain is preserved once the original asset is added as an ingredient. The ingredient carries the original's manifest data, so validators can trace the full history. If the original is not added as an ingredient, the provenance chain is broken: the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). ### Quick reference decision tree ```mermaid flowchart TD - Q1{Need to read an\nexisting manifest?} - Q1 -->|No| USE_B[Use Builder alone new manifest from scratch] + Q1{Need to read an existing manifest?} + Q1 -->|No| USE_B["Use Builder alone (new manifest from scratch)"] Q1 -->|Yes| Q2{Need to create a new/modified manifest?} - Q2 -->|No| USE_R[Use Reader alone inspect/extract only] + Q2 -->|No| USE_R["Use Reader alone (inspect/extract only)"] Q2 -->|Yes| USE_BR[Use both Reader + Builder] - USE_BR --> Q3{What to keep?} - Q3 -->|Everything| P1["add_ingredient() with original asset"] - Q3 -->|Some parts| P2["Read JSON, filter, create new Builder"] - Q3 -->|Nothing| P3["New Builder alone fresh manifest"] + USE_BR --> Q3{What to keep from the existing manifest?} + Q3 -->|Everything| P1["add_ingredient() with original asset or archive path"] + Q3 -->|Some parts| P2["1. Read: reader.json() + get_resource() 2. Filter: pick ingredients & actions to keep 3. Build: new Builder with filtered JSON 4. Transfer: .add_resource for kept binaries 5. Sign: builder.sign()"] + Q3 -->|Nothing| P3["New Builder alone (fresh manifest, no prior provenance)"] ``` diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index 4418cde4..f2bedb87 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -3772,3 +3772,749 @@ TEST_F(BuilderTest, ExtractIngredientsFromArchives) { // Reset settings c2pa::load_settings(R"({"verify": {"verify_after_reading": true}})", "json"); } + +TEST_F(BuilderTest, AddMultipleActions) +{ + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto context = c2pa::Context(); + auto builder = c2pa::Builder(context, manifest); + + builder.add_action(R"({ + "action": "c2pa.color_adjustments", + "parameters": { "name": "brightnesscontrast" } + })"); + + builder.add_action(R"({ + "action": "c2pa.filtered", + "parameters": { "name": "A filter" }, + "description": "Filtering applied" + })"); + + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + auto output_path = get_temp_path("multiple_actions.jpg"); + + std::vector manifest_data; + ASSERT_NO_THROW(manifest_data = builder.sign(source_path, output_path, signer)); + + auto reader = c2pa::Reader(context, output_path); + auto parsed = json::parse(reader.json()); + string active = parsed["active_manifest"]; + + bool found_color = false, found_filter = false; + for (auto& assertion : parsed["manifests"][active]["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.color_adjustments") found_color = true; + if (action["action"] == "c2pa.filtered") { + found_filter = true; + EXPECT_EQ(action["description"], "Filtering applied"); + } + } + } + } + EXPECT_TRUE(found_color) << "c2pa.color_adjustments not found"; + EXPECT_TRUE(found_filter) << "c2pa.filtered not found"; +} + +TEST_F(BuilderTest, LinkActionToIngredient) +{ + auto context = c2pa::Context(); + + std::string instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"; + + json manifest_json = { + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.created"}, + {"digitalSourceType", "http://c2pa.org/digitalsourcetype/compositeCapture"} + }, + { + {"action", "c2pa.placed"}, + {"description", "Placed image into composition"}, + {"parameters", { + {"ingredientIds", json::array({instance_id})} + }} + } + })} + }} + } + })} + }; + + auto builder = c2pa::Builder(context, manifest_json.dump()); + + json ingredient = { + {"title", "source_asset.jpg"}, + {"relationship", "componentOf"}, + {"instance_id", instance_id} + }; + builder.add_ingredient(ingredient.dump(), c2pa_test::get_fixture_path("A.jpg")); + + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + auto output_path = get_temp_path("link_action.jpg"); + + std::vector manifest_data; + ASSERT_NO_THROW(manifest_data = builder.sign(source_path, output_path, signer)); + + // Read back and verify the ingredientIds were resolved to JUMBF URLs + auto reader = c2pa::Reader(context, output_path); + auto parsed = json::parse(reader.json()); + string active = parsed["active_manifest"]; + + bool found_linked_action = false; + for (auto& assertion : parsed["manifests"][active]["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.placed" && + action.contains("parameters") && + action["parameters"].contains("ingredients")) { + auto& ings = action["parameters"]["ingredients"]; + ASSERT_GE(ings.size(), 1u); + EXPECT_TRUE(ings[0].contains("url")); + // URL should be a JUMBF path + std::string url = ings[0]["url"]; + EXPECT_TRUE(url.find("c2pa.ingredient") != std::string::npos) + << "URL should reference an ingredient assertion, got: " << url; + found_linked_action = true; + } + } + } + } + EXPECT_TRUE(found_linked_action) << "Linked c2pa.placed action not found"; +} + +TEST_F(BuilderTest, ExtractIngredientsFromArchiveAndReuse) +{ + auto context = c2pa::Context(); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + + // Step 1: Build a working store with ingredients and archive it + auto builder = c2pa::Builder(context, manifest); + builder.add_ingredient(R"({"title": "A.jpg", "relationship": "componentOf"})", + c2pa_test::get_fixture_path("A.jpg")); + builder.add_ingredient(R"({"title": "C.jpg", "relationship": "componentOf"})", + c2pa_test::get_fixture_path("C.jpg")); + + std::stringstream archive_stream(std::ios::in | std::ios::out | std::ios::binary); + ASSERT_NO_THROW(builder.to_archive(archive_stream)); + + // Step 2: Read the archive and extract only one of the 2 ingredients + archive_stream.seekg(0); + c2pa::Reader reader(context, "application/c2pa", archive_stream); + auto parsed = json::parse(reader.json()); + std::string active = parsed["active_manifest"]; + auto ingredients = parsed["manifests"][active]["ingredients"]; + + ASSERT_GE(ingredients.size(), 2u) << "Archive should have 2 ingredients"; + + // Pick only ingredient with title A.jpg (this is one way of filtering) + json selected = json::array(); + for (auto& ingredient : ingredients) { + if (ingredient["title"] == "A.jpg") { + selected.push_back(ingredient); + } + } + + // Step 3: Create a new Builder with only selected ingredients + json new_manifest = json::parse(manifest); + new_manifest["ingredients"] = selected; + auto new_builder = c2pa::Builder(context, new_manifest.dump()); + + // Transfer binary resources + for (auto& ingredient : selected) { + if (ingredient.contains("thumbnail") && ingredient["thumbnail"].contains("identifier")) { + std::string id = ingredient["thumbnail"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + new_builder.add_resource(id, stream); + } + if (ingredient.contains("manifest_data") && + ingredient["manifest_data"].is_object() && + ingredient["manifest_data"].contains("identifier")) { + std::string id = ingredient["manifest_data"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + new_builder.add_resource(id, stream); + } + } + + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + auto output_path = get_temp_path("extracted_ingredient.jpg"); + + std::vector manifest_data; + ASSERT_NO_THROW(manifest_data = new_builder.sign(source_path, output_path, signer)); + + // Verify the output has only 1 ingredient + auto out_reader = c2pa::Reader(context, output_path); + auto out_parsed = json::parse(out_reader.json()); + std::string out_active = out_parsed["active_manifest"]; + auto out_ingredients = out_parsed["manifests"][out_active]["ingredients"]; + EXPECT_EQ(out_ingredients.size(), 1u) << "Output should have exactly 1 ingredient"; + EXPECT_EQ(out_ingredients[0]["title"], "A.jpg"); +} + +TEST_F(BuilderTest, FilterActionsWithLinkedIngredients) +{ + // When filtering, we keep c2pa.created (required by spec) + // and c2pa.placed (wanted) + its ingredient + auto context = c2pa::Context(); + + std::string instance_id = "xmp:iid:abcd1234-ef56-7890-abcd-ef1234567890"; + + // Build a manifest with c2pa.created (spec mandatory), + // c2pa.placed (linked), + // and c2pa.filtered (to be dropped) + json manifest_json = { + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.created"}, + {"digitalSourceType", "http://c2pa.org/digitalsourcetype/compositeCapture"} + }, + { + {"action", "c2pa.placed"}, + {"description", "Placed an image"}, + {"parameters", { + {"ingredientIds", json::array({instance_id})} + }} + }, + { + {"action", "c2pa.filtered"}, + {"parameters", {{"name", "blur"}}} + } + })} + }} + } + })} + }; + + auto builder = c2pa::Builder(context, manifest_json.dump()); + + json ingredient = { + {"title", "asset.jpg"}, + {"relationship", "componentOf"}, + {"instance_id", instance_id} + }; + builder.add_ingredient(ingredient.dump(), c2pa_test::get_fixture_path("A.jpg")); + + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + auto signed_path = get_temp_path("linked_source.jpg"); + + ASSERT_NO_THROW(builder.sign(source_path, signed_path, signer)); + + // Read the signed asset and filter: + // Keep c2pa.created (required), + // keep c2pa.placed (wanted with ingredient), + // drop c2pa.filtered + auto reader = c2pa::Reader(context, signed_path); + auto parsed = json::parse(reader.json()); + string active = parsed["active_manifest"]; + auto source_manifest = parsed["manifests"][active]; + + // Filter actions and track needed ingredients + json kept_actions = json::array(); + std::set needed_labels; + + for (auto& assertion : source_manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + std::string action_type = action["action"]; + // Always keep required actions (c2pa.created / c2pa.opened) + if (action_type == "c2pa.created" || action_type == "c2pa.opened") { + kept_actions.push_back(action); + } + // Keep c2pa.placed (wanted) and track its linked ingredients + else if (action_type == "c2pa.placed") { + kept_actions.push_back(action); + if (action.contains("parameters") && + action["parameters"].contains("ingredients")) { + for (auto& ref : action["parameters"]["ingredients"]) { + std::string url = ref["url"]; + std::string label = url.substr(url.rfind('/') + 1); + needed_labels.insert(label); + } + } + } + // Drop everything else (c2pa.filtered, etc.) + } + } + } + + ASSERT_GE(kept_actions.size(), 2u) << "Should have kept c2pa.created + c2pa.placed"; + ASSERT_FALSE(needed_labels.empty()) << "c2pa.placed should reference at least one ingredient"; + + // Keep only ingredients referenced by kept actions + json kept_ingredients = json::array(); + for (auto& ing : source_manifest["ingredients"]) { + if (ing.contains("label") && needed_labels.count(ing["label"])) { + kept_ingredients.push_back(ing); + } + } + ASSERT_FALSE(kept_ingredients.empty()) << "Should have kept at least one ingredient"; + + // Build the new manifest + json new_manifest = json::parse(R"({ + "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}] + })"); + new_manifest["ingredients"] = kept_ingredients; + new_manifest["assertions"] = json::array({ + { + {"label", "c2pa.actions"}, + {"data", {{"actions", kept_actions}}} + } + }); + + auto new_builder = c2pa::Builder(context, new_manifest.dump()); + + // Transfer binary resources for kept ingredients + for (auto& ing : kept_ingredients) { + if (ing.contains("thumbnail") && ing["thumbnail"].contains("identifier")) { + std::string id = ing["thumbnail"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + new_builder.add_resource(id, stream); + } + if (ing.contains("manifest_data") && + ing["manifest_data"].is_object() && + ing["manifest_data"].contains("identifier")) { + std::string id = ing["manifest_data"]["identifier"]; + std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, stream); + stream.seekg(0); + new_builder.add_resource(id, stream); + } + } + + auto output_path = get_temp_path("filtered_linked.jpg"); + ASSERT_NO_THROW(new_builder.sign(source_path, output_path, signer)); + + // Verify: output has c2pa.created + c2pa.placed but NOT c2pa.filtered + auto out_reader = c2pa::Reader(context, output_path); + auto out_parsed = json::parse(out_reader.json()); + string out_active = out_parsed["active_manifest"]; + + bool found_created = false, found_placed = false; + for (auto& assertion : out_parsed["manifests"][out_active]["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.created") found_created = true; + if (action["action"] == "c2pa.placed") found_placed = true; + EXPECT_NE(action["action"], "c2pa.filtered") + << "c2pa.filtered should have been filtered out"; + } + } + } + EXPECT_TRUE(found_created) << "c2pa.created must be present (required for valid v2)"; + EXPECT_TRUE(found_placed) << "c2pa.placed should be in the output"; + + // Verify the output has the linked ingredient + auto out_ingredients = out_parsed["manifests"][out_active]["ingredients"]; + EXPECT_GE(out_ingredients.size(), 1u) << "Should have at least 1 ingredient"; +} + +TEST_F(BuilderTest, WalkTree) +{ + auto context = c2pa::Context(); + auto manifest = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + + // Layer 1: Create a new asset with c2pa.created + c2pa.color_adjustments + auto builder1 = c2pa::Builder(context, manifest); + builder1.add_action(R"({"action": "c2pa.color_adjustments", "parameters": {"name": "contrast"}})"); + auto signed1_path = get_temp_path("tree_layer1.jpg"); + ASSERT_NO_THROW(builder1.sign(source_path, signed1_path, signer)); + + // Layer 2: Open layer1 (c2pa.opened + parentOf ingredient) and add c2pa.filtered + std::string instance_id = "tree-layer1"; + json layer2_manifest = { + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.opened"}, + {"parameters", { + {"ingredientIds", json::array({instance_id})} + }} + } + })} + }} + } + })} + }; + auto builder2 = c2pa::Builder(context, layer2_manifest.dump()); + builder2.add_action(R"({"action": "c2pa.filtered", "parameters": {"name": "sharpen"}})"); + builder2.add_ingredient( + json({{"title", "layer1.jpg"}, {"relationship", "parentOf"}, {"instance_id", instance_id}}).dump(), + signed1_path); + auto signed2_path = get_temp_path("tree_layer2.jpg"); + ASSERT_NO_THROW(builder2.sign(source_path, signed2_path, signer)); + + // Read the final asset and walk the tree + auto reader = c2pa::Reader(context, signed2_path); + auto parsed = json::parse(reader.json()); + string active = parsed["active_manifest"]; + auto active_manifest = parsed["manifests"][active]; + + // The active manifest should have c2pa.opened and c2pa.filtered + bool found_opened = false, found_filtered = false; + for (auto& assertion : active_manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.opened") found_opened = true; + if (action["action"] == "c2pa.filtered") found_filtered = true; + } + } + } + EXPECT_TRUE(found_opened) << "Active manifest should have c2pa.opened"; + EXPECT_TRUE(found_filtered) << "Active manifest should have c2pa.filtered"; + + // Walk into the ingredient's manifest (the tree) + ASSERT_TRUE(active_manifest.contains("ingredients")); + ASSERT_FALSE(active_manifest["ingredients"].empty()); + + bool found_ingredient_with_manifest = false; + for (auto& ingredient : active_manifest["ingredients"]) { + if (ingredient.contains("active_manifest")) { + found_ingredient_with_manifest = true; + std::string ing_label = ingredient["active_manifest"]; + + // The ingredient's manifest should be in the flat manifests dictionary + ASSERT_TRUE(parsed["manifests"].contains(ing_label)) + << "Ingredient manifest should be in the manifests dictionary"; + + auto ing_manifest = parsed["manifests"][ing_label]; + + // The ingredient's manifest should have c2pa.created and c2pa.color_adjustments + bool found_created = false, found_contrast = false; + for (auto& assertion : ing_manifest["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.created") found_created = true; + if (action["action"] == "c2pa.color_adjustments") { + found_contrast = true; + EXPECT_EQ(action["parameters"]["name"], "contrast"); + } + } + } + } + EXPECT_TRUE(found_created) + << "Ingredient manifest should have c2pa.created from layer 1"; + EXPECT_TRUE(found_contrast) + << "Ingredient manifest should have c2pa.color_adjustments from layer 1"; + } + } + EXPECT_TRUE(found_ingredient_with_manifest) + << "Should have at least one ingredient with its own manifest (tree structure)"; +} + +TEST_F(BuilderTest, AddIngredientsUsingLabelParentOfComponentOf) +{ + auto context = c2pa::Context(); + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + + // The labels used for linking may not be the ones that will be in the + // manifest. They are just indicators on which ingredient to link with what. + auto manifest_json = R"( + { + "claim_generator_info": [{ "name": "TestAddIngredientsUsingLabelParentOfComponentOf", "version": "0.1" }], + "assertions": [ + { + "label": "c2pa.actions.v2", + "data": { + "actions": [ + { + "action": "c2pa.opened", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_1"] + } + }, + { + "action": "c2pa.placed", + "parameters": { + "ingredientIds": ["c2pa.ingredient.v3_2"] + } + } + ] + } + } + ] + } + )"; + + auto builder = c2pa::Builder(context, manifest_json); + + auto ingredient_json_1 = R"( + { + "title": "C.jpg", + "format": "image/jpeg", + "relationship": "parentOf", + "label": "c2pa.ingredient.v3_1" + } + )"; + + auto ingredient_json_2 = R"( + { + "title": "A.jpg", + "format": "image/jpeg", + "relationship": "componentOf", + "label": "c2pa.ingredient.v3_2" + } + )"; + + builder.add_ingredient(ingredient_json_1, c2pa_test::get_fixture_path("C.jpg")); + builder.add_ingredient(ingredient_json_2, c2pa_test::get_fixture_path("A.jpg")); + + auto output_path = get_temp_path("signed_labelled_ingredient_3.jpg"); + ASSERT_NO_THROW(builder.sign(source_path, output_path, signer)); + + auto reader = c2pa::Reader(context, output_path); + auto reader_json = reader.json(); + ASSERT_TRUE(reader_json.find("TestAddIngredientsUsingLabelParentOfComponentOf") != std::string::npos); + + // Verify both ingredients are present with correct relationships + auto parsed = json::parse(reader_json); + std::string active = parsed["active_manifest"]; + auto& ingredients = parsed["manifests"][active]["ingredients"]; + ASSERT_GE(ingredients.size(), 2u); + + // Find each ingredient by title and check relationship + bool found_parent = false, found_component = false; + for (auto& ing : ingredients) { + if (ing["title"] == "C.jpg") { + EXPECT_EQ(ing["relationship"], "parentOf"); + found_parent = true; + } + if (ing["title"] == "A.jpg") { + EXPECT_EQ(ing["relationship"], "componentOf"); + found_component = true; + } + } + EXPECT_TRUE(found_parent) << "Should have parentOf ingredient (C.jpg)"; + EXPECT_TRUE(found_component) << "Should have componentOf ingredient (A.jpg)"; + + // Verify both actions resolved their ingredientIds + for (auto& assertion : parsed["manifests"][active]["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.opened" || action["action"] == "c2pa.placed") { + ASSERT_TRUE(action.contains("parameters")); + ASSERT_TRUE(action["parameters"].contains("ingredients")) + << action["action"].get() + << " should have resolved ingredientIds to ingredients[] URLs"; + } + } + } + } +} + +TEST_F(BuilderTest, AddIngredientFromArchiveWithOverride) +{ + auto context = c2pa::Context(); + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + + // Step 1: Create a Builder with an ingredient titled "my-first-title", + // then archive it to a .c2pa file (ingredient archive) + auto manifest_str = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto builder1 = c2pa::Builder(context, manifest_str); + builder1.add_ingredient( + R"({"title": "my-first-title", "relationship": "componentOf"})", + c2pa_test::get_fixture_path("A.jpg")); + + auto archive_path = get_temp_path("example-ingredient.c2pa"); + ASSERT_NO_THROW(builder1.to_archive(archive_path)); + ASSERT_TRUE(fs::exists(archive_path)); + + // Step 2: Create a new Builder and add the ingredient from the .c2pa archive, + // but override the title to "overridden-title" + auto builder2 = c2pa::Builder(context, manifest_str); + json ingredient_override = { + {"title", "overridden-title"}, + {"relationship", "parentOf"} + }; + builder2.add_ingredient(ingredient_override.dump(), archive_path); + + auto output_path = get_temp_path("archive_override_result.jpg"); + ASSERT_NO_THROW(builder2.sign(source_path, output_path, signer)); + + // Step 3: Read back and verify which title ended up in the signed manifest + auto reader = c2pa::Reader(context, output_path); + auto parsed = json::parse(reader.json()); + std::string active = parsed["active_manifest"]; + auto ingredients = parsed["manifests"][active]["ingredients"]; + + ASSERT_GE(ingredients.size(), 1u); + auto& ing = ingredients[0]; + + // Check whether the override title or the original archive title won + std::string actual_title = ing["title"]; + std::string actual_relationship = ing["relationship"]; + + // Verify the override title is what ends up in the signed manifest + EXPECT_EQ(actual_title, "overridden-title") + << "Title from add_ingredient JSON should override the archive's original title"; + EXPECT_EQ(actual_relationship, "parentOf") + << "Relationship from add_ingredient JSON should override the archive's original relationship"; +} + +TEST_F(BuilderTest, AddIngredientFromArchiveWithCustomProperties) +{ + auto context = c2pa::Context(); + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + + // Step 1: Create an ingredient archive with a basic ingredient + auto manifest_str = c2pa_test::read_text_file(c2pa_test::get_fixture_path("training.json")); + auto builder1 = c2pa::Builder(context, manifest_str); + builder1.add_ingredient( + R"({"title": "original.jpg", "relationship": "componentOf"})", + c2pa_test::get_fixture_path("A.jpg")); + + auto archive_path = get_temp_path("custom-props-ingredient.c2pa"); + ASSERT_NO_THROW(builder1.to_archive(archive_path)); + + // Step 2: Add the archived ingredient with overridden + additional custom properties + std::string custom_instance = "tracking:proj-7:asset-42"; + json ingredient_with_custom_props = { + {"title", "renamed-asset.jpg"}, + {"relationship", "parentOf"}, + {"instance_id", custom_instance}, + {"description", "Overridden ingredient with extra properties"}, + {"informational_URI", "https://example.com/assets/42"} + }; + + auto builder2 = c2pa::Builder(context, manifest_str); + builder2.add_ingredient(ingredient_with_custom_props.dump(), archive_path); + + auto output_path = get_temp_path("archive_custom_props_result.jpg"); + ASSERT_NO_THROW(builder2.sign(source_path, output_path, signer)); + + // Step 3: Verify all properties survived signing + auto reader = c2pa::Reader(context, output_path); + auto parsed = json::parse(reader.json()); + std::string active = parsed["active_manifest"]; + auto ingredients = parsed["manifests"][active]["ingredients"]; + + ASSERT_GE(ingredients.size(), 1u); + auto& ing = ingredients[0]; + + EXPECT_EQ(ing["title"], "renamed-asset.jpg") + << "Title override should be preserved"; + EXPECT_EQ(ing["relationship"], "parentOf") + << "Relationship override should be preserved"; + + // Check which additional custom properties survived + if (ing.contains("instance_id")) { + EXPECT_EQ(ing["instance_id"], custom_instance); + } + if (ing.contains("description")) { + EXPECT_EQ(ing["description"], "Overridden ingredient with extra properties"); + } + if (ing.contains("informational_URI")) { + EXPECT_EQ(ing["informational_URI"], "https://example.com/assets/42"); + } +} + +TEST_F(BuilderTest, CustomParamsInActions) +{ + auto context = c2pa::Context(); + auto signer = c2pa_test::create_test_signer(); + auto source_path = c2pa_test::get_fixture_path("A.jpg"); + + std::string instance_id = "custom-param-test-1"; + + json manifest_json = { + {"claim_generator_info", json::array({{{"name", "c2pa-test"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.created"}, + {"digitalSourceType", "http://c2pa.org/digitalsourcetype/compositeCapture"}, + {"parameters", { + {"test.example.tool", "my-tool"}, + {"test.example.session_id", "session-abc-123"} + }} + }, + { + {"action", "c2pa.placed"}, + {"description", "Placed an image"}, + {"parameters", { + {"test.example.layer_id", "layer-42"}, + {"ingredientIds", json::array({instance_id})} + }} + } + })} + }} + } + })} + }; + + auto builder = c2pa::Builder(context, manifest_json.dump()); + builder.add_ingredient( + json({{"title", "photo.jpg"}, {"relationship", "componentOf"}, {"instance_id", instance_id}}).dump(), + c2pa_test::get_fixture_path("A.jpg")); + + auto output_path = get_temp_path("vendor_params.jpg"); + ASSERT_NO_THROW(builder.sign(source_path, output_path, signer)); + + // Read back and verify custom params survive signing + auto reader = c2pa::Reader(context, output_path); + auto parsed = json::parse(reader.json()); + string active = parsed["active_manifest"]; + + bool found_created_params = false; + bool found_placed_params = false; + for (auto& assertion : parsed["manifests"][active]["assertions"]) { + if (assertion["label"] == "c2pa.actions.v2") { + for (auto& action : assertion["data"]["actions"]) { + if (action["action"] == "c2pa.created" && action.contains("parameters")) { + auto& params = action["parameters"]; + if (params.contains("test.example.tool")) { + EXPECT_EQ(params["test.example.tool"], "my-tool"); + EXPECT_EQ(params["test.example.session_id"], "session-abc-123"); + found_created_params = true; + } + } + if (action["action"] == "c2pa.placed" && action.contains("parameters")) { + auto& params = action["parameters"]; + if (params.contains("test.example.layer_id")) { + EXPECT_EQ(params["test.example.layer_id"], "layer-42"); + found_placed_params = true; + } + // Also verify ingredientIds resolved to URL + EXPECT_TRUE(params.contains("ingredients")) + << "ingredientIds should be resolved to ingredients[] URLs"; + } + } + } + } + EXPECT_TRUE(found_created_params) + << "Custom params on c2pa.created should survive signing"; + EXPECT_TRUE(found_placed_params) + << "Custom params on c2pa.placed should survive signing"; +} From 4276a45fe4fbe18a1fd2f2b3c4814b86efb3a2ab Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Wed, 25 Feb 2026 22:07:06 -0800 Subject: [PATCH 12/16] fix: typos --- docs/selective-manifests.md | 11 ++++++++--- tests/builder.test.cpp | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 3c0f8986..876619f4 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -780,15 +780,20 @@ The `Reader` returns a manifest store, which is a dictionary of manifests. But c ```mermaid flowchart TD subgraph Store["Manifest Store"] - M1["Active Manifest\n- assertions (including c2pa.actions.v2), ingredients"] - M2["Ingredient A's manifest with its own c2pa.actions.v2 and its own ingredients"] - M3["Ingredient B's manifest with its own c2pa.actions.v2"] + M1["Active Manifest\n- assertions (including c2pa.actions.v2)\n- ingredients"] + M2["Ingredient A's manifest\n- its own c2pa.actions.v2\n- its own ingredients"] + M3["Ingredient B's manifest\n- its own c2pa.actions.v2"] end M1 -->|"ingredient A has manifest_data"| M2 M1 -->|"ingredient B has manifest_data"| M3 + M1 -.-|"ingredient C has no manifest_data"| M5["Ingredient C\n(unsigned asset, no provenance)"] M2 -->|"may have its own ingredients..."| M4["...deeper in the tree"] + + style M5 fill:#eee,stroke:#999,stroke-dasharray: 5 5 ``` +Not every ingredient has provenance. An unsigned asset added as an ingredient will have a `title`, `format`, and `relationship`, but no `manifest_data` and no entry in the `"manifests"` dictionary. It appears in the `"ingredients"` array but has no `"active_manifest"` field. + The `reader.json()` returns all manifests in a flattened `"manifests"` dictionary, keyed by their label (a URN like `contentauth:urn:uuid:...`). The `"active_manifest"` key indicates which one is the top of the tree. Each manifest in the tree has its assertions and ingredients. Walking the tree reveals the full provenance chain: what each actor did at each step, including what actions were performed and what ingredients were used. diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index a036481d..9ac8fbbd 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -4517,6 +4517,8 @@ TEST_F(BuilderTest, CustomParamsInActions) << "Custom params on c2pa.created should survive signing"; EXPECT_TRUE(found_placed_params) << "Custom params on c2pa.placed should survive signing"; +} + TEST_F(BuilderTest, NonAsciiSourcePathForSign) { // Copy A.jpg to a temp path with a non-ASCII name rather than relying on From 918b817d6d7fd2b4cda18d9273d3ec098133180c Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 26 Feb 2026 17:22:42 -0800 Subject: [PATCH 13/16] Pull Q&A into separate file --- docs/faqs.md | 121 ++++++++++++++++++++++++++++++++++++ docs/selective-manifests.md | 101 +++++++++++++++++++----------- 2 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 docs/faqs.md diff --git a/docs/faqs.md b/docs/faqs.md new file mode 100644 index 00000000..f6e50645 --- /dev/null +++ b/docs/faqs.md @@ -0,0 +1,121 @@ +# FAQs + +## When to use a `Reader` vs. `Builder` + +## Quick reference decision tree + +```mermaid +flowchart TD + Q1{Need to read an existing manifest?} + Q1 -->|No| USE_B["Use Builder alone (new manifest from scratch)"] + Q1 -->|Yes| Q2{Need to create a new/modified manifest?} + Q2 -->|No| USE_R["Use Reader alone (inspect/extract only)"] + Q2 -->|Yes| USE_BR[Use both Reader + Builder] + USE_BR --> Q3{What to keep from the existing manifest?} + Q3 -->|Everything| P1["add_ingredient() with original asset or archive path"] + Q3 -->|Some parts| P2["1. Read: reader.json() + get_resource() 2. Filter: pick ingredients & actions to keep 3. Build: new Builder with filtered JSON 4. Transfer: .add_resource for kept binaries 5. Sign: builder.sign()"] + Q3 -->|Nothing| P3["New Builder alone (fresh manifest, no prior provenance)"] +``` + +### When to use `Reader` + +**Use a `Reader` when the goal is only to inspect or extract data without creating a new manifest.** + +- Validating whether an asset has C2PA credentials +- Displaying provenance information to a user +- Extracting thumbnails for display +- Checking trust status and validation results +- Inspecting ingredient chains + +```cpp +c2pa::Reader reader(context, "image.jpg"); +auto json = reader.json(); // inspect the manifest +reader.get_resource(thumb_id, stream); // extract a thumbnail +``` + +The `Reader` is read-only. It never modifies the source asset. + +### When to use a `Builder` + +**Use a `Builder` when creating a manifest from scratch on an asset that has no existing C2PA data, or when intentionally starting with a clean slate.** + +- Signing a brand-new asset for the first time +- Adding C2PA credentials to an unsigned asset +- Creating a manifest with all content defined from scratch + +```cpp +c2pa::Builder builder(context, manifest_json); +builder.add_ingredient(ingredient_json, source_path); // add source material +builder.sign(source_path, output_path, signer); +``` + +Every call to the `Builder` constructor or `Builder::from_archive()` creates a new `Builder`. There is no way to modify an existing signed manifest directly. + +### When to use both `Reader` and `Builder` together + +**Use both when filtering content from an existing manifest into a new one. The `Reader` extracts data, application code filters it, and a new `Builder` receives only the selected parts.** + +- Filtering specific ingredients from a manifest +- Dropping specific assertions while keeping others +- Filtering actions (keeping some, removing others) +- Merging ingredients from multiple signed assets or archives +- Extracting content from an ingredients catalog +- Re-signing with different settings while keeping some original content + +```cpp +// Read existing (does not modify the asset) +c2pa::Reader reader(context, "signed.jpg"); +auto parsed = json::parse(reader.json()); + +// Filter what to keep +auto kept = filter(parsed); // application-specific filtering logic + +// Create a new Builder with only the filtered content +c2pa::Builder builder(context, kept.dump()); +// ... transfer resources ... +builder.sign(source, output, signer); +``` + +## How should I add ingredients? + +There are two ways: using `add_ingredient()` and injecting ingredient JSON via `with_definition()`. The table below summarizes these options. + +| Approach | What it does | When to use | +| --- | --- | --- | +| `add_ingredient(json, path)` | Reads the source (a signed asset, an unsigned file, or a `.c2pa` archive), extracts its manifest store automatically, generates a thumbnail | Adding an ingredient where the library should handle extraction. Works with ingredient catalog archives too: pass the archive path and the library extracts the manifest data | +| Inject via `with_definition()` + `add_resource()` | Accepts the ingredient JSON and all binary resources provided manually | Reconstructing from a reader or merging from multiple readers, where the data has already been extracted | + + +## When to use archives + +There are two distinct archive concepts: + +- **Builder archives (Working store archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: + - Signing must happen on a different machine (e.g., an HSM server) + - Checkpointing work-in-progress before signing + - Transmitting a `Builder` state across a network boundary + +- **Ingredient archives** contain the manifest store data (`.c2pa` binary) from ingredients that were added to a `Builder`. When a signed asset is added as an ingredient via `add_ingredient()`, the library extracts and stores its manifest store as `manifest_data` within the ingredient record. When the `Builder` is then serialized via `to_archive()`, these ingredient manifest stores are included. Use ingredient archives when: + + - Building an ingredients catalog for pick-and-choose workflows + - Preserving provenance history from source assets + - Transferring ingredient data between `Reader` and `Builder` + +See also [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores). + +Key consideration for builder archives: `from_archive()` creates a new `Builder` with **default** context settings. If specific settings are needed (e.g., thumbnails disabled), use `with_archive()` on a `Builder` that already has the desired context: + +```cpp +// Preserves the caller's context settings +c2pa::Builder builder(my_context); +builder.with_archive(archive_stream); +builder.sign(source, output, signer); +``` + +## Can a manifest be modified in place? + +**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the desired changes and sign it. This is by design: it ensures the integrity of the provenance chain. + +## What happens to the provenance chain when rebuilding a working store? + +When creating a new manifest, the chain is preserved once the original asset is added as an ingredient. The ingredient carries the original's manifest data, so validators can trace the full history. If the original is not added as an ingredient, the provenance chain is broken: the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). \ No newline at end of file diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 876619f4..03b38bdf 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,14 +1,16 @@ # Selective manifest construction with Builder and Reader -`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is useful in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). This process is best described as filtering, or rebuilding a working store: +`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is useful in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). + +This process is best described as *filtering*, or *rebuilding* a working store, and consists of these steps: 1. Read an existing manifest. -1. Choose which elements to retain. -1. Build a new manifest containing only those elements. +2. Choose which elements to retain. +3. Build a new manifest containing only those elements. -A C2PA manifest is a signed data structure attached to an asset (such as an image or video) that records provenance information: who created it, what tools were used, what edits were made, and what source assets (ingredients) contributed to it. A manifest contains assertions (statements about the asset), ingredients (references to other assets used to create the signed asset), and binary resources (like thumbnails). +A manifest is a signed data structure attached to an asset that records provenance information and what source assets (ingredients) contributed to it. A manifest contains assertions (statements about the asset), ingredients (references to other assets used to create the signed asset), and binary resources (like thumbnails). -Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), the way to "remove" content is to **read what exists, filter out what is needed, and create a new `Builder` with only that information added back on the new Builder**. This process produces a new `Builder` instance ("rebuild"). +Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), to remove content you must **read what exists, filter out what is needed, and create a new** `Builder` **with only that information added back on the new Builder**. This process produces a new `Builder` instance ("rebuild"). > **Important**: This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. @@ -22,6 +24,8 @@ flowchart LR D -->|sign| E[New Asset] ``` + + The fundamental workflow is: 1. **Read** the existing manifest with `Reader` to get JSON and binary resources @@ -54,7 +58,7 @@ auto thumbnail_id = manifest["thumbnail"]["identifier"]; ### Extracting binary resources -The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. The actual binary content must be extracted separately using `get_resource()`: +The JSON returned by `reader.json()` only contains string identifiers (JUMBF URIs) for binary data like thumbnails and ingredient manifest stores. Extract the actual binary content by using `get_resource()`: ```cpp // Extract a thumbnail to a stream @@ -71,7 +75,7 @@ Each example below creates a **new `Builder`** from filtered data. The original When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what is registered with `builder.add_resource()`. -### Example 1: Keep only specific ingredients +### Keep only specific ingredients ```cpp c2pa::Context context; @@ -118,7 +122,7 @@ for (auto& ingredient : kept_ingredients) { builder.sign(source_path, output_path, signer); ``` -### Example 2: Keep only specific assertions +### Keep only specific assertions ```cpp auto assertions = parsed["manifests"][active]["assertions"]; @@ -141,7 +145,7 @@ c2pa::Builder builder(context, new_manifest.dump()); builder.sign(source_path, output_path, signer); ``` -### Example 3: Start fresh and preserve provenance +### Start fresh and preserve provenance Sometimes all existing assertions and ingredients may need to be discarded but the provenance chain should be maintained nevertheless. This is done by creating a new `Builder` with a new manifest definition and adding the original signed asset as an ingredient using `add_ingredient()`. @@ -168,6 +172,8 @@ flowchart TD style NI fill:#efe,stroke:#090 ``` + + ```cpp // Create a new Builder with a new definition c2pa::Builder builder(context); @@ -203,23 +209,25 @@ builder.add_action(R"({ ### Action JSON fields -| Field | Required | Description | -|-------|----------|-------------| -| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | -| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | -| `description` | No | Human-readable description of what happened | -| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | + +| Field | Required | Description | +| ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | +| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | +| `description` | No | Human-readable description of what happened | +| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | + ### Linking actions to ingredients -When an action involves a specific ingredient, the ingredient and action need to be linked. This is done using `ingredientIds` in the action's parameters, which references a matching key on the ingredient. +When an action involves a specific ingredient, the ingredient is linked to the action using `ingredientIds` (in the action's `parameters`), referencing a matching key in the ingredient. #### How `ingredientIds` resolution works The SDK matches each value in `ingredientIds` against ingredients using this priority: -1. **`label`** on the ingredient (primary): if set and non-empty, this is used as the linking key -2. **`instance_id`** on the ingredient (fallback): used when `label` is absent or empty +1. `label` on the ingredient (primary): if set and non-empty, this is used as the linking key. +2. `instance_id` on the ingredient (fallback): used when `label` is absent or empty. #### Linking with `label` @@ -323,9 +331,10 @@ builder.add_ingredient(R"({ builder.sign(source_path, output_path, signer); ``` -> **Note:** The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. +> [!NOTE] +> The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. -#### Linking with `instance_id` (fallback) +#### Linking with `instance_id` When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. @@ -367,7 +376,8 @@ builder.add_ingredient(ingredient.dump(), source_photo_path); builder.sign(source_path, output_path, signer); ``` -> Note: The `instance_id` can be read back from the ingredient JSON after signing. +> [!NOTE] +> The `instance_id` can be read back from the ingredient JSON after signing. #### After signing: reading linked ingredients back @@ -403,15 +413,17 @@ for (auto& assertion : manifest["assertions"]) { } ``` -#### `label` vs `instance_id`: when to use which? +#### `When to use label` vs `instance_id` + + +| Property | `label` | `instance_id` | +| -------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | +| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | +| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | +| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | -| Property | `label` | `instance_id` | -| -------- | ------- | ------------- | -| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | -| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | -| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | -| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | -| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | **Use `label`** when defining manifests in JSON. **Use `instance_id`** when working programmatically with ingredients whose identity comes from other sources, or when a stable identifier that persists unchanged across rebuilds is needed. @@ -456,6 +468,8 @@ flowchart TD style X fill:#f99,stroke:#c00 ``` + + ```cpp // Read from a catalog of archived ingredients // verify_after_reading is not needed for newer versions of the SDK @@ -582,7 +596,7 @@ for (auto& action : actions) { } ``` -> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components following the pattern `[a-zA-Z0-9][a-zA-Z0-9_-]*` (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (eg. `c2pa` or `cawg`) may be reserved. +> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components following the pattern `[a-zA-Z0-9][a-zA-Z0-9_-]`* (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (eg. `c2pa` or `cawg`) may be reserved. ### Extracting ingredients from a working store into archives @@ -605,6 +619,8 @@ flowchart TD end ``` + + **Step 1:** Build a working store and archive it: ```cpp @@ -792,6 +808,8 @@ flowchart TD style M5 fill:#eee,stroke:#999,stroke-dasharray: 5 5 ``` + + Not every ingredient has provenance. An unsigned asset added as an ingredient will have a `title`, `format`, and `relationship`, but no `manifest_data` and no entry in the `"manifests"` dictionary. It appears in the `"ingredients"` array but has no `"active_manifest"` field. The `reader.json()` returns all manifests in a flattened `"manifests"` dictionary, keyed by their label (a URN like `contentauth:urn:uuid:...`). The `"active_manifest"` key indicates which one is the top of the tree. @@ -859,6 +877,8 @@ flowchart TD NB -->|sign| OUT["New with 2 actions only: opened, placed"] ``` + + ### Basic action filtering When filtering, remember that the first action must remain `c2pa.created` or `c2pa.opened` for the manifest to be valid. If the first action is removed, a new one must be added. @@ -907,7 +927,7 @@ builder.sign(source_path, output_path, signer); Some actions reference ingredients (via `parameters.ingredients[].url` after signing). If keeping an action that references an ingredient, **the corresponding ingredient and its binary resources must also be kept**. If an ingredient is dropped, any actions that reference it must also be dropped (or updated). -#### Understanding the `c2pa.opened` action +#### `c2pa.opened` action The `c2pa.opened` action is special because it must be the first action and it references the asset that was opened (the `parentOf` ingredient). When filtering: @@ -915,7 +935,7 @@ The `c2pa.opened` action is special because it must be the first action and it r - **Keep the ingredient it references**: the `parentOf` ingredient linked via its `parameters.ingredients[].url` - Removing the ingredient that `c2pa.opened` points to will make the manifest invalid -#### Understanding the `c2pa.placed` action +#### `c2pa.placed` action The `c2pa.placed` action references a `componentOf` ingredient that was composited into the asset. When filtering: @@ -1032,6 +1052,8 @@ flowchart LR end ``` + + ```cpp c2pa::Builder builder(context, manifest_json); builder.set_no_embed(); @@ -1081,6 +1103,8 @@ flowchart TD end ``` + + ## Q&A: Builder, Reader, or both? This section answers questions about when to use each API and how they work together. @@ -1146,10 +1170,12 @@ builder.sign(source, output, signer); ### What is the difference between `add_ingredient()` and injecting ingredient JSON via `with_definition()`? -| Approach | What it does | When to use | -|----------|-------------|-------------| -| `add_ingredient(json, path)` | Reads the source (a signed asset, an unsigned file, or a `.c2pa` archive), extracts its manifest store automatically, generates a thumbnail | Adding an ingredient where the library should handle extraction. Works with ingredient catalog archives too: pass the archive path and the library extracts the manifest data | -| Inject via `with_definition()` + `add_resource()` | Accepts the ingredient JSON and all binary resources provided manually | Reconstructing from a reader or merging from multiple readers, where the data has already been extracted | + +| Approach | What it does | When to use | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `add_ingredient(json, path)` | Reads the source (a signed asset, an unsigned file, or a `.c2pa` archive), extracts its manifest store automatically, generates a thumbnail | Adding an ingredient where the library should handle extraction. Works with ingredient catalog archives too: pass the archive path and the library extracts the manifest data | +| Inject via `with_definition()` + `add_resource()` | Accepts the ingredient JSON and all binary resources provided manually | Reconstructing from a reader or merging from multiple readers, where the data has already been extracted | + ### When to use archives @@ -1198,3 +1224,6 @@ flowchart TD Q3 -->|Some parts| P2["1. Read: reader.json() + get_resource() 2. Filter: pick ingredients & actions to keep 3. Build: new Builder with filtered JSON 4. Transfer: .add_resource for kept binaries 5. Sign: builder.sign()"] Q3 -->|Nothing| P3["New Builder alone (fresh manifest, no prior provenance)"] ``` + + + From 6b2257d0825d3fa0e75824aa3558a44029893d93 Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Thu, 26 Feb 2026 17:42:14 -0800 Subject: [PATCH 14/16] Forgot to save files --- docs/faqs.md | 4 +- docs/selective-manifests.md | 124 ------------------------------------ 2 files changed, 2 insertions(+), 126 deletions(-) diff --git a/docs/faqs.md b/docs/faqs.md index f6e50645..51315a2e 100644 --- a/docs/faqs.md +++ b/docs/faqs.md @@ -1,6 +1,6 @@ # FAQs -## When to use a `Reader` vs. `Builder` +## When do I use `Reader` vs. `Builder` ## Quick reference decision tree @@ -90,7 +90,7 @@ There are two ways: using `add_ingredient()` and injecting ingredient JSON via ` There are two distinct archive concepts: -- **Builder archives (Working store archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: +- **Builder archives (working store archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: - Signing must happen on a different machine (e.g., an HSM server) - Checkpointing work-in-progress before signing - Transmitting a `Builder` state across a network boundary diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 03b38bdf..70b8c5b8 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1103,127 +1103,3 @@ flowchart TD end ``` - - -## Q&A: Builder, Reader, or both? - -This section answers questions about when to use each API and how they work together. - -### When to use a `Reader` - -**Use a `Reader` when the goal is only to inspect or extract data without creating a new manifest.** - -- Validating whether an asset has C2PA credentials -- Displaying provenance information to a user -- Extracting thumbnails for display -- Checking trust status and validation results -- Inspecting ingredient chains - -```cpp -c2pa::Reader reader(context, "image.jpg"); -auto json = reader.json(); // inspect the manifest -reader.get_resource(thumb_id, stream); // extract a thumbnail -``` - -The `Reader` is read-only. It never modifies the source asset. - -### When to use a `Builder` - -**Use a `Builder` when creating a manifest from scratch on an asset that has no existing C2PA data, or when intentionally starting with a clean slate.** - -- Signing a brand-new asset for the first time -- Adding C2PA credentials to an unsigned asset -- Creating a manifest with all content defined from scratch - -```cpp -c2pa::Builder builder(context, manifest_json); -builder.add_ingredient(ingredient_json, source_path); // add source material -builder.sign(source_path, output_path, signer); -``` - -Every call to the `Builder` constructor or `Builder::from_archive()` creates a new `Builder`. There is no way to modify an existing signed manifest directly. - -### When to use both `Reader` and `Builder` together - -**Use both when filtering content from an existing manifest into a new one. The `Reader` extracts data, application code filters it, and a new `Builder` receives only the selected parts.** - -- Filtering specific ingredients from a manifest -- Dropping specific assertions while keeping others -- Filtering actions (keeping some, removing others) -- Merging ingredients from multiple signed assets or archives -- Extracting content from an ingredients catalog -- Re-signing with different settings while keeping some original content - -```cpp -// Read existing (does not modify the asset) -c2pa::Reader reader(context, "signed.jpg"); -auto parsed = json::parse(reader.json()); - -// Filter what to keep -auto kept = filter(parsed); // application-specific filtering logic - -// Create a new Builder with only the filtered content -c2pa::Builder builder(context, kept.dump()); -// ... transfer resources ... -builder.sign(source, output, signer); -``` - -### What is the difference between `add_ingredient()` and injecting ingredient JSON via `with_definition()`? - - -| Approach | What it does | When to use | -| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `add_ingredient(json, path)` | Reads the source (a signed asset, an unsigned file, or a `.c2pa` archive), extracts its manifest store automatically, generates a thumbnail | Adding an ingredient where the library should handle extraction. Works with ingredient catalog archives too: pass the archive path and the library extracts the manifest data | -| Inject via `with_definition()` + `add_resource()` | Accepts the ingredient JSON and all binary resources provided manually | Reconstructing from a reader or merging from multiple readers, where the data has already been extracted | - - -### When to use archives - -There are two distinct archive concepts (see also [Working stores](https://opensource.contentauthenticity.org/docs/rust-sdk/docs/working-stores)): - -**Builder archives (Working store archives)** (`to_archive` / `from_archive` / `with_archive`) serialize the full `Builder` state (manifest definition, resources, ingredients) so it can be resumed or signed later, possibly on a different machine or in a different process. The archive is not yet signed. Use builder archives when: - -- Signing must happen on a different machine (e.g., an HSM server) -- Checkpointing work-in-progress before signing -- Transmitting a `Builder` state across a network boundary - -**Ingredient archives** contain the manifest store data (`.c2pa` binary) from ingredients that were added to a `Builder`. When a signed asset is added as an ingredient via `add_ingredient()`, the library extracts and stores its manifest store as `manifest_data` within the ingredient record. When the `Builder` is then serialized via `to_archive()`, these ingredient manifest stores are included. Use ingredient archives when: - -- Building an ingredients catalog for pick-and-choose workflows -- Preserving provenance history from source assets -- Transferring ingredient data between `Reader` and `Builder` - -Key consideration for builder archives: `from_archive()` creates a new `Builder` with **default** context settings. If specific settings are needed (e.g., thumbnails disabled), use `with_archive()` on a `Builder` that already has the desired context: - -```cpp -// Preserves the caller's context settings -c2pa::Builder builder(my_context); -builder.with_archive(archive_stream); -builder.sign(source, output, signer); -``` - -### Can a manifest be modified in place? - -**No.** C2PA manifests are cryptographically signed. Any modification invalidates the signature. The only way to "modify" a manifest is to create a new `Builder` with the desired changes and sign it. This is by design: it ensures the integrity of the provenance chain. - -### What happens to the provenance chain when rebuilding a working store? - -When creating a new manifest, the chain is preserved once the original asset is added as an ingredient. The ingredient carries the original's manifest data, so validators can trace the full history. If the original is not added as an ingredient, the provenance chain is broken: the new manifest has no link to the original. This might be intentional (starting fresh) or a mistake (losing provenance). - -### Quick reference decision tree - -```mermaid -flowchart TD - Q1{Need to read an existing manifest?} - Q1 -->|No| USE_B["Use Builder alone (new manifest from scratch)"] - Q1 -->|Yes| Q2{Need to create a new/modified manifest?} - Q2 -->|No| USE_R["Use Reader alone (inspect/extract only)"] - Q2 -->|Yes| USE_BR[Use both Reader + Builder] - USE_BR --> Q3{What to keep from the existing manifest?} - Q3 -->|Everything| P1["add_ingredient() with original asset or archive path"] - Q3 -->|Some parts| P2["1. Read: reader.json() + get_resource() 2. Filter: pick ingredients & actions to keep 3. Build: new Builder with filtered JSON 4. Transfer: .add_resource for kept binaries 5. Sign: builder.sign()"] - Q3 -->|Nothing| P3["New Builder alone (fresh manifest, no prior provenance)"] -``` - - - From 6daf33bfb4debcf63349be5cebbff51d3eeabcda Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Fri, 27 Feb 2026 10:54:40 -0800 Subject: [PATCH 15/16] General edits --- docs/selective-manifests.md | 151 +++++++++++++++++------------------- 1 file changed, 70 insertions(+), 81 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 70b8c5b8..66ce731a 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,16 +1,16 @@ # Selective manifest construction with Builder and Reader -`Builder` and `Reader` can be used together to selectively construct manifests, keeping only the parts needed and leaving out the rest. This is useful in the case where not all ingredients on a working store should be added (e.g. in the case where ingredient assets are not visible). +`Builder` and `Reader` can be used together to selectively construct manifests—keeping only the parts you need and omitting the rest. This is useful when not all ingredients in a working store should be included (for example, when some ingredient assets are not visible). -This process is best described as *filtering*, or *rebuilding* a working store, and consists of these steps: +This process is best described as *filtering* or *rebuilding* a working store: 1. Read an existing manifest. 2. Choose which elements to retain. 3. Build a new manifest containing only those elements. -A manifest is a signed data structure attached to an asset that records provenance information and what source assets (ingredients) contributed to it. A manifest contains assertions (statements about the asset), ingredients (references to other assets used to create the signed asset), and binary resources (like thumbnails). +A manifest is a signed data structure attached to an asset that records provenance and which source assets (ingredients) contributed to it. It contains assertions (statements about the asset), ingredients (references to other assets), and references to binary resources (such as thumbnails). -Since both `Reader` and `Builder` are **read-only by design** (there is no `remove()` method on either), to remove content you must **read what exists, filter out what is needed, and create a new** `Builder` **with only that information added back on the new Builder**. This process produces a new `Builder` instance ("rebuild"). +Since both `Reader` and `Builder` are **read-only by design** (neither has a `remove()` method), to exclude content you must **read what exists, filter to keep what you need, and create a new** `Builder` **with only that information**. This produces a new `Builder` instance—a "rebuild." > **Important**: This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. @@ -35,8 +35,7 @@ The fundamental workflow is: ## Reading an existing manifest -Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). -The `Reader` does not modify the source asset in any way. +Use `Reader` to extract the manifest store JSON and any binary resources (thumbnails, manifest data). The source asset is never modified. ```cpp c2pa::Context context; @@ -71,9 +70,11 @@ reader.get_resource(thumbnail_id, fs::path("thumbnail.jpg")); ## Filtering into a new Builder -Each example below creates a **new `Builder`** from filtered data. The original asset (and its manifest store) remains as is. +Each example below creates a **new `Builder`** from filtered data. The original asset and its manifest store are never modified. -When rebuilding by transferring ingredients between a `Reader` and a **new** `Builder`, remember that both the JSON metadata and the associated binary resources (thumbnails, manifest data) must be transferred. The JSON contains identifiers that reference binary resources. Those identifiers must match what is registered with `builder.add_resource()`. +When transferring ingredients from a `Reader` to a new `Builder`, you must transfer both the JSON metadata and the associated binary resources (thumbnails, manifest data). The JSON contains identifiers that reference those resources; the same identifiers must be used when calling `builder.add_resource()`. + +> **Transferring binary resources:** For each kept ingredient, call `reader.get_resource(id, stream)` for any `thumbnail` or `manifest_data` it contains, then `builder.add_resource(id, stream)` with the same identifier. ### Keep only specific ingredients @@ -98,23 +99,21 @@ new_manifest["ingredients"] = kept_ingredients; c2pa::Builder builder(context, new_manifest.dump()); -// Transfer binary resources for kept ingredients only +// Transfer binary resources for kept ingredients (see note above) for (auto& ingredient : kept_ingredients) { - // Transfer thumbnail if (ingredient.contains("thumbnail")) { - std::string thumb_id = ingredient["thumbnail"]["identifier"]; - std::stringstream thumb(std::ios::in | std::ios::out | std::ios::binary); - reader.get_resource(thumb_id, thumb); - thumb.seekg(0); - builder.add_resource(thumb_id, thumb); + std::string id = ingredient["thumbnail"]["identifier"]; + std::stringstream s(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, s); + s.seekg(0); + builder.add_resource(id, s); } - // Transfer manifest_data (the ingredient's own C2PA manifest) if (ingredient.contains("manifest_data")) { - std::string md_id = ingredient["manifest_data"]["identifier"]; - std::stringstream md(std::ios::in | std::ios::out | std::ios::binary); - reader.get_resource(md_id, md); - md.seekg(0); - builder.add_resource(md_id, md); + std::string id = ingredient["manifest_data"]["identifier"]; + std::stringstream s(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(id, s); + s.seekg(0); + builder.add_resource(id, s); } } @@ -192,7 +191,7 @@ builder.sign(source_path, output_path, signer); ## Adding actions to a working store -Actions record what was done to an asset (e.g. color adjustments, cropping, placing content). Use `builder.add_action()` to add actions to a working store. +Actions record what was done to an asset (e.g., color adjustments, cropping, placing content). Use `builder.add_action()` to add them to a working store. ```cpp builder.add_action(R"({ @@ -341,7 +340,7 @@ When no `label` is set on an ingredient, the SDK matches `ingredientIds` against ```cpp c2pa::Context context; -// instance_id is used as identified for the ingredient to link, and needs to be unique +// instance_id is used as the linking identifier and must be unique std::string instance_id = "xmp:iid:939a4c48-0dff-44ec-8f95-61f52b11618f"; json manifest_json = { @@ -413,8 +412,7 @@ for (auto& assertion : manifest["assertions"]) { } ``` -#### `When to use label` vs `instance_id` - +#### When to use `label` vs `instance_id` | Property | `label` | `instance_id` | | -------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | @@ -438,13 +436,13 @@ There are two distinct types of archives, sharing the same binary format but bei A **builder archive** (also called a working store archive) is a serialized snapshot of a `Builder`. It contains the manifest definition, all resources, and any ingredients that were added. It is created by `builder.to_archive()` and restored with `Builder::from_archive()` or `builder.with_archive()`. -An **ingredient archive** contains the manifest store data from an asset that was added as an ingredient. +An **ingredient archive** contains the manifest store from an asset that was added as an ingredient. -The key difference: a builder archive is a work-in-progress (unsigned). An ingredient archive carries the provenance history of a source asset to reuse as ingredient in other working stores. +The key difference: a builder archive is a work-in-progress (unsigned). An ingredient archive carries the provenance history of a source asset for reuse as an ingredient in other working stores. ### The ingredients catalog pattern -A usage example of ingredient archives is building an **ingredients catalog**: a collection of archived ingredients (with 1 ingredient per archive) that can be picked and chosen from when constructing a final manifest. Each archive in the catalog holds ingredients, and at build time the caller selects only the ones needed. +An **ingredients catalog** is a collection of archived ingredients that can be selected when constructing a final manifest. Each archive holds ingredients; at build time the caller selects only the ones needed. ```mermaid flowchart TD @@ -472,9 +470,7 @@ flowchart TD ```cpp // Read from a catalog of archived ingredients -// verify_after_reading is not needed for newer versions of the SDK -// c2pa::Context archive_ctx(R"({"verify": {"verify_after_reading": false}})"); -c2pa::Context archive_ctx(R"<>"); +c2pa::Context archive_ctx; // Add settings if needed, e.g. verify options // Open one archive from the catalog archive_stream.seekg(0); @@ -533,11 +529,11 @@ json ingredient_override = { builder.add_ingredient(ingredient_override.dump(), signed_asset_path); ``` -The `title`, `relationship`, and `instance_id` fields in the provided JSON take priority. The library fills in everything else (thumbnail, manifest_data, format) automatically from the source asset. This works with any source: a signed asset, a `.c2pa` archive, or an unsigned file. +The `title`, `relationship`, and `instance_id` fields in the provided JSON take priority. The library fills in the rest (thumbnail, manifest_data, format) from the source. This works with signed assets, `.c2pa` archives, or unsigned files. ### Using custom vendor parameters in actions -The C2PA specification allows **vendor-namespaced parameters** on actions using reverse domain notation. These custom parameters survive signing and can be read back, making them useful for tagging actions with IDs that help with filtering. +The C2PA specification allows **vendor-namespaced parameters** on actions using reverse domain notation. These parameters survive signing and can be read back, useful for tagging actions with IDs that support filtering. ```cpp auto manifest_json = R"( @@ -596,7 +592,7 @@ for (auto& action : actions) { } ``` -> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components following the pattern `[a-zA-Z0-9][a-zA-Z0-9_-]`* (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (eg. `c2pa` or `cawg`) may be reserved. +> **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (e.g., `c2pa` or `cawg`) may be reserved. ### Extracting ingredients from a working store into archives @@ -688,55 +684,70 @@ new_builder.sign(source_path, output_path, signer); ### Merging multiple working stores -In some cases it may be necessary to merge ingredients from multiple working stores (builder archives) or multiple working stores into a single `Builder`. This should be a **fallback strategy** as the recommended practice is to maintain a **single** active working store and reuse it by adding ingredients incrementally (which is where having archived ingredients catalogs can be helpful). Merging is available as a backup when multiple working stores need to be consolidated. +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. -When merging from multiple sources, resource identifier URIs can collide. One way to avoid collisions is to rename identifiers with a unique suffix: +When merging from multiple sources, resource identifier URIs can collide. Rename identifiers with a unique suffix when needed. Use two passes: (1) collect ingredients with collision handling, build the manifest, create the builder; (2) re-read each archive and transfer resources (use original ID for `get_resource()`, renamed ID for `add_resource()` when collisions occurred). ```cpp -// Track used resource IDs to detect collisions std::set used_ids; int suffix_counter = 0; - json all_ingredients = json::array(); +std::vector> archive_info; // (stream, ingredient count) +// Pass 1: Collect ingredients, renaming IDs on collision for (auto& archive_stream : archives) { + archive_stream.seekg(0); c2pa::Reader reader(archive_ctx, "application/c2pa", archive_stream); auto parsed = json::parse(reader.json()); - std::string active = parsed["active_manifest"]; - auto ingredients = parsed["manifests"][active]["ingredients"]; + auto ingredients = parsed["manifests"][parsed["active_manifest"]]["ingredients"]; for (auto& ingredient : ingredients) { - // Check for thumbnail ID collision - if (ingredient.contains("thumbnail")) { - std::string id = ingredient["thumbnail"]["identifier"]; + for (const char* key : {"thumbnail", "manifest_data"}) { + if (!ingredient.contains(key)) continue; + std::string id = ingredient[key]["identifier"]; if (used_ids.count(id)) { - std::string new_id = id + "__" + std::to_string(++suffix_counter); - ingredient["thumbnail"]["identifier"] = new_id; - id = new_id; + ingredient[key]["identifier"] = id + "__" + std::to_string(++suffix_counter); } - used_ids.insert(id); - // Transfer resource with the (possibly renamed) ID - std::stringstream stream(std::ios::in | std::ios::out | std::ios::binary); - reader.get_resource(id, stream); - stream.seekg(0); - builder.add_resource(id, stream); + used_ids.insert(ingredient[key]["identifier"].get()); } all_ingredients.push_back(ingredient); } + archive_info.emplace_back(&archive_stream, ingredients.size()); } -// Create a single new Builder with all merged ingredients json manifest = json::parse(R"({ "claim_generator_info": [{"name": "an-application", "version": "0.1.0"}] })"); manifest["ingredients"] = all_ingredients; c2pa::Builder builder(context, manifest.dump()); + +// Pass 2: Transfer resources (match by ingredient index) +size_t idx = 0; +for (auto& [stream, count] : archive_info) { + stream->seekg(0); + c2pa::Reader reader(archive_ctx, "application/c2pa", *stream); + auto parsed = json::parse(reader.json()); + auto orig = parsed["manifests"][parsed["active_manifest"]]["ingredients"]; + + for (size_t i = 0; i < count; ++i) { + auto& o = orig[i]; + auto& m = all_ingredients[idx++]; + for (const char* key : {"thumbnail", "manifest_data"}) { + if (!o.contains(key)) continue; + std::stringstream s(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(o[key]["identifier"].get(), s); + s.seekg(0); + builder.add_resource(m[key]["identifier"].get(), s); + } + } +} + builder.sign(source_path, output_path, signer); ``` ## Retrieving actions from a working store -Use `Reader` to read actions back from an asset, or an archived Builder. Actions are stored in the `c2pa.actions.v2` assertion inside the manifest. +Actions are stored in the `c2pa.actions.v2` assertion. Use `Reader` to extract them from a signed asset or an archived Builder. ### Reading actions @@ -763,35 +774,17 @@ for (auto& assertion : assertions) { ### Reading actions from an archive -The same approach works for archived Builders. The format parameter `"application/c2pa"` tells the `Reader` to read from an archive stream instead of an asset: +Use the same approach with format `"application/c2pa"` and an archive stream: ```cpp -c2pa::Context context; - -// Read from an archive instead of an asset std::ifstream archive_file("builder_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 assertions = parsed["manifests"][active]["assertions"]; - -// Find the actions assertion -- same as reading from an asset -for (auto& assertion : assertions) { - if (assertion["label"] == "c2pa.actions.v2") { - auto actions = assertion["data"]["actions"]; - for (auto& action : actions) { - std::cout << "Action: " << action["action"] << std::endl; - if (action.contains("description")) { - std::cout << " Description: " << action["description"] << std::endl; - } - } - } -} +// Then parse and iterate assertions as in the example above ``` ### Understanding the manifest tree -The `Reader` returns a manifest store, which is a dictionary of manifests. But conceptually, it represents a tree (graph without cycles): each manifest may have ingredients and assertions, and each ingredient can carry its own manifest store (in its `manifest_data` field), which in turn can have its own ingredients, actions, assertions, and so on recursively. +The `Reader` returns a manifest store—a dictionary of manifests keyed by label (a URN like `contentauth:urn:uuid:...`). Conceptually it forms a tree: each manifest has assertions and ingredients; ingredients with `manifest_data` carry their own manifest store, which can have its own ingredients and assertions recursively. The `active_manifest` key indicates the root. ```mermaid flowchart TD @@ -810,11 +803,7 @@ flowchart TD -Not every ingredient has provenance. An unsigned asset added as an ingredient will have a `title`, `format`, and `relationship`, but no `manifest_data` and no entry in the `"manifests"` dictionary. It appears in the `"ingredients"` array but has no `"active_manifest"` field. - -The `reader.json()` returns all manifests in a flattened `"manifests"` dictionary, keyed by their label (a URN like `contentauth:urn:uuid:...`). The `"active_manifest"` key indicates which one is the top of the tree. - -Each manifest in the tree has its assertions and ingredients. Walking the tree reveals the full provenance chain: what each actor did at each step, including what actions were performed and what ingredients were used. +Not every ingredient has provenance. An unsigned asset added as an ingredient has `title`, `format`, and `relationship`, but no `manifest_data` and no entry in the `"manifests"` dictionary. Walking the tree reveals the full provenance chain: what each actor did at each step, including actions performed and ingredients used. **To walk the tree and find actions at each level:** @@ -867,7 +856,7 @@ for (auto& ingredient : active_manifest["ingredients"]) { ## Filtering actions (keeping some, removing others) -Since there is no `remove()` method, the way to remove actions is to **read, pick the ones to keep, create a new Builder**. +To remove actions, use the same read–filter–rebuild pattern: **read, pick the ones to keep, create a new Builder**. ```mermaid flowchart TD From 319d0160e37e679d90b8a7fa4e823bd61efb818d Mon Sep 17 00:00:00 2001 From: Rand McKinney Date: Fri, 27 Feb 2026 11:09:06 -0800 Subject: [PATCH 16/16] More edits, make headings more concise --- docs/selective-manifests.md | 62 ++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index 66ce731a..b22f4d0a 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -1,6 +1,6 @@ -# Selective manifest construction with Builder and Reader +# Selective manifest construction -`Builder` and `Reader` can be used together to selectively construct manifests—keeping only the parts you need and omitting the rest. This is useful when not all ingredients in a working store should be included (for example, when some ingredient assets are not visible). +You can use `Builder` and `Reader` together to selectively construct manifests—keeping only the parts you need and omitting the rest. This is useful when you don't want to include all ingredients in a working store (for example, when some ingredient assets are not visible). This process is best described as *filtering* or *rebuilding* a working store: @@ -10,9 +10,10 @@ This process is best described as *filtering* or *rebuilding* a working store: A manifest is a signed data structure attached to an asset that records provenance and which source assets (ingredients) contributed to it. It contains assertions (statements about the asset), ingredients (references to other assets), and references to binary resources (such as thumbnails). -Since both `Reader` and `Builder` are **read-only by design** (neither has a `remove()` method), to exclude content you must **read what exists, filter to keep what you need, and create a new** `Builder` **with only that information**. This produces a new `Builder` instance—a "rebuild." +Since both `Reader` and `Builder` are **read-only** by design (neither has a `remove()` method), to exclude content you must **read what exists, filter to keep what you need, and create a new** `Builder` **with only that information**. This produces a new `Builder` instance—a "rebuild." -> **Important**: This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. +> [!IMPORTANT] +> This process always creates a new `Builder`. The original signed asset and its manifest are never modified, neither is the starting working store. The `Reader` extracts data without side effects, and the `Builder` constructs a new manifest based on extracted data. ## Core concepts @@ -209,12 +210,12 @@ builder.add_action(R"({ ### Action JSON fields -| Field | Required | Description | -| ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | -| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | -| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | -| `description` | No | Human-readable description of what happened | -| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | +| Field | Required | Description | +| --- | --- | --- | +| `action` | Yes | Action identifier, e.g. `"c2pa.created"`, `"c2pa.opened"`, `"c2pa.placed"`, `"c2pa.color_adjustments"`, `"c2pa.filtered"` | +| `parameters` | No | Free-form object with action-specific data (including `ingredientIds` for linking ingredients, for instance) | +| `description` | No | Human-readable description of what happened | +| `digitalSourceType` | Sometimes, depending on action | URI describing the digital source type (typically for `c2pa.created`) | ### Linking actions to ingredients @@ -276,9 +277,12 @@ builder.add_ingredient(ingredient_json, photo_path); builder.sign(source_path, output_path, signer); ``` -##### Multiple ingredients need distinct identifying labels +##### Linking multiple ingredients -When linking multiple ingredients, each ingredient needs a unique label: +When linking multiple ingredients, each ingredient needs a unique label. + +> [!NOTE] +> The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. ```cpp auto manifest_json = R"( @@ -330,9 +334,6 @@ builder.add_ingredient(R"({ builder.sign(source_path, output_path, signer); ``` -> [!NOTE] -> The labels used for linking in the working store may not be the exact labels that appear in the signed manifest. They are indicators for the SDK to know which ingredient to link with which action. The SDK assigns final labels during signing. - #### Linking with `instance_id` When no `label` is set on an ingredient, the SDK matches `ingredientIds` against `instance_id`. @@ -378,7 +379,7 @@ builder.sign(source_path, output_path, signer); > [!NOTE] > The `instance_id` can be read back from the ingredient JSON after signing. -#### After signing: reading linked ingredients back +#### Reading linked ingredients After signing, `ingredientIds` is gone. The action's `parameters.ingredients[]` contains hashed JUMBF URIs pointing to ingredient assertions. To match an action to its ingredient, extract the label from the URL: @@ -414,13 +415,13 @@ for (auto& assertion : manifest["assertions"]) { #### When to use `label` vs `instance_id` -| Property | `label` | `instance_id` | -| -------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | -| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | -| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | -| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | -| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | +| Property | `label` | `instance_id` | +| --- | --- | --- | +| **Who controls it** | Caller (any string) | Caller (any string, or from XMP metadata) | +| **Priority for linking** | Primary: checked first | Fallback: used when label is absent/empty | +| **When to use** | JSON-defined manifests where the caller controls the ingredient definition | Programmatic workflows using `read_ingredient_file()` or XMP-based IDs | +| **Survives signing** | SDK may reassign the actual assertion label | Unchanged | +| **Stable across rebuilds** | The caller controls the build-time value; the post-signing label may change | Yes, always the same set value | **Use `label`** when defining manifests in JSON. @@ -515,9 +516,9 @@ for (auto& ingredient : selected) { builder.sign(source_path, output_path, signer); ``` -### Overriding ingredient properties when adding from an archive +### 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: +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: ```cpp // Override title, relationship, and set a custom instance_id for tracking @@ -594,7 +595,7 @@ for (auto& action : actions) { > **Naming convention:** Vendor parameters must use reverse domain notation with period-separated components (e.g., `com.mycompany.tool`, `net.example.session_id`). Some namespaces (e.g., `c2pa` or `cawg`) may be reserved. -### Extracting ingredients from a working store into archives +### Extracting ingredients from a working store An example workflow is to build up a working store with multiple ingredients, archive it, and then later extract specific ingredients from that archive to use in a new working store. @@ -854,7 +855,7 @@ for (auto& ingredient : active_manifest["ingredients"]) { } ``` -## Filtering actions (keeping some, removing others) +## Filtering actions To remove actions, use the same read–filter–rebuild pattern: **read, pick the ones to keep, create a new Builder**. @@ -932,7 +933,9 @@ The `c2pa.placed` action references a `componentOf` ingredient that was composit - If the ingredient is dropped, also drop the `c2pa.placed` action - If `c2pa.placed` is not required: it can safely be removed (and the ingredient it references, if it is the only reference) -#### Full example: filtering with linked ingredients +#### Example + +The code below provides an example of filtering with linked ingredients. ```cpp c2pa::Context context; @@ -1020,7 +1023,8 @@ for (auto& ingredient : kept_ingredients) { builder.sign(source_path, output_path, signer); ``` -> **Note:** When copying ingredient JSON objects from a reader, they keep their `label` field. Since the action URLs reference ingredients by label, the links resolve correctly as long as ingredients are not renamed or reindexed. If ingredients are re-added via `add_ingredient()` (which generates new labels), the action URLs will also need to be updated. +> [!NOTE] +> When copying ingredient JSON objects from a reader, they keep their `label` field. Since the action URLs reference ingredients by label, the links resolve correctly as long as ingredients are not renamed or reindexed. If ingredients are re-added via `add_ingredient()` (which generates new labels), the action URLs will also need to be updated. ## Controlling manifest embedding