From 029ae78fe828e44eb64ff0dfcf20174df00ef8d5 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 10 Mar 2026 14:46:11 -0700 Subject: [PATCH 1/3] fix: The amalgam --- scripts/amalgamate.cmake | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/amalgamate.cmake b/scripts/amalgamate.cmake index 85a0852..6932713 100644 --- a/scripts/amalgamate.cmake +++ b/scripts/amalgamate.cmake @@ -49,8 +49,15 @@ set(C2PA_INTERNAL_HPP "${C2PA_SOURCE_DIR}/c2pa_internal.hpp") if(NOT EXISTS "${C2PA_INTERNAL_HPP}") message(FATAL_ERROR "amalgamate.cmake: c2pa_internal.hpp not found: ${C2PA_INTERNAL_HPP}") endif() -file(READ "${C2PA_INTERNAL_HPP}" INTERNAL_CONTENT) -file(APPEND "${C2PA_AMALGAM_OUTPUT}" "${INTERNAL_CONTENT}") +file(STRINGS "${C2PA_INTERNAL_HPP}" INTERNAL_LINES) +set(INTERNAL_FILTERED "") +foreach(LINE IN LISTS INTERNAL_LINES) + string(FIND "${LINE}" "#include \"" IDX) + if(IDX LESS 0) + string(APPEND INTERNAL_FILTERED "${LINE}\n") + endif() +endforeach() +file(APPEND "${C2PA_AMALGAM_OUTPUT}" "${INTERNAL_FILTERED}") file(APPEND "${C2PA_AMALGAM_OUTPUT}" "\n") # Handle includes manually From 71d52fa8011ad319a276b47c915de1a697705ecc Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 10 Mar 2026 15:26:06 -0700 Subject: [PATCH 2/3] fix: Bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8558663..330d5e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ cmake_minimum_required(VERSION 3.27) # This is the version of this C++ project -project(c2pa-c VERSION 0.15.0) +project(c2pa-c VERSION 0.15.1) # Set the version of the c2pa_rs library used set(C2PA_VERSION "0.77.0") From 2a56ac0425fb89a750bfda476589c8f56c68eeee Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:11:27 -0700 Subject: [PATCH 3/3] fix: More docs (#175) Co-authored-by: Tania Mathern --- docs/selective-manifests.md | 168 ++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/docs/selective-manifests.md b/docs/selective-manifests.md index b22f4d0..30e6561 100644 --- a/docs/selective-manifests.md +++ b/docs/selective-manifests.md @@ -683,6 +683,174 @@ for (auto& ingredient : selected) { new_builder.sign(source_path, output_path, signer); ``` +### Reading ingredient details from an ingredient archive + +An ingredient archive is a serialized `Builder` containing exactly one and only one ingredient (see [Builder archives vs. ingredient archives](#builder-archives-vs-ingredient-archives)). Reading it with `Reader` allows the caller to inspect the ingredient before deciding whether to use it: its thumbnail, whether it carries provenance (e.g. an active manifest), validation status, relationship, etc. + +```mermaid +flowchart LR + IA["ingredient_archive.c2pa"] -->|"Reader(application/c2pa)"| JSON["JSON + resources"] + JSON --> TH["Thumbnail"] + JSON --> AM["Active manifest?"] + JSON --> VS["Validation status"] + JSON --> REL["Relationship"] +``` + +```cpp +// Open the ingredient archive +std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary); +c2pa::Reader reader(context, "application/c2pa", archive_file); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto manifest = parsed["manifests"][active]; + +// An ingredient archive has exactly one ingredient +auto& ingredient = manifest["ingredients"][0]; + +// Relationship +std::string relationship = ingredient["relationship"]; // e.g. "parentOf", "componentOf", "inputTo" + +// Instance ID (optional, set by the caller via add_ingredient or derived from XMP metadata) +std::string instance_id; +if (ingredient.contains("instance_id")) { + instance_id = ingredient["instance_id"]; +} + +// Active manifest: +// When present, the ingredient was a signed asset and its manifest label +// points into the top-level "manifests" dictionary. +bool has_provenance = ingredient.contains("active_manifest"); +if (has_provenance) { + std::string ing_manifest_label = ingredient["active_manifest"]; + auto ing_manifest = parsed["manifests"][ing_manifest_label]; + // ing_manifest contains the ingredient's own assertions, actions, etc. +} + +// Validation status. +// The top-level "validation_status" array covers the entire manifest store, +// including this ingredient's manifest. An empty or absent array means +// no validation errors were found. +if (parsed.contains("validation_status")) { + for (auto& status : parsed["validation_status"]) { + std::cout << status["code"].get() << ": " + << status["explanation"].get() << std::endl; + } +} + +// Thumbnail +if (ingredient.contains("thumbnail")) { + std::string thumb_id = ingredient["thumbnail"]["identifier"]; + std::stringstream thumb_stream(std::ios::in | std::ios::out | std::ios::binary); + reader.get_resource(thumb_id, thumb_stream); + // thumb_stream now contains the thumbnail binary data +} +``` + +#### Linking an archived ingredient to an action + +After reading the ingredient details from an ingredient archive, the ingredient can be added to a new `Builder` and linked to an action. The preferred approach is to assign a `label` in the `add_ingredient` call and use that label as the linking key in `ingredientIds`. If the archived ingredient carries an `instance_id`, you can use that instead. + +Note that labels are only used as build-time linking keys. The SDK may reassign the actual label in the signed manifest. An `instance_id`, on the other hand, is preserved as-is through signing and can be read back unchanged from the final manifest. + +##### Using a label + +Assign a `label` in the `add_ingredient` call and reference that same label in `ingredientIds`. This works whether or not the ingredient has an `instance_id`. + +```cpp +c2pa::Context context; + +// Read the ingredient archive +std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary); +c2pa::Reader reader(context, "application/c2pa", archive_file); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto& ingredient = parsed["manifests"][active]["ingredients"][0]; + +// Use a caller-assigned label as the linking key +json manifest_json = { + {"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions.v2"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.opened"}, + {"parameters", { + {"ingredientIds", json::array({"archived-ingredient"})} + }} + } + })} + }} + } + })} +}; + +c2pa::Builder builder(context, manifest_json.dump()); + +// The label on the ingredient JSON matches the entry in ingredientIds +archive_file.seekg(0); +builder.add_ingredient( + json({ + {"title", ingredient["title"]}, + {"relationship", "parentOf"}, + {"label", "archived-ingredient"} + }).dump(), + "application/c2pa", + archive_file); + +builder.sign(source_path, output_path, signer); +``` + +##### Using an `instance_id` + +If the ingredient archive carries an `instance_id` and you need a stable identifier that persists unchanged in the signed manifest, you can use the `instance_id` as the linking key in `ingredientIds` instead of a label. + +```cpp +c2pa::Context context; + +// Read the ingredient archive and extract the instance_id +std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary); +c2pa::Reader reader(context, "application/c2pa", archive_file); +auto parsed = json::parse(reader.json()); +std::string active = parsed["active_manifest"]; +auto& ingredient = parsed["manifests"][active]["ingredients"][0]; +std::string instance_id = ingredient["instance_id"]; + +json manifest_json = { + {"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})}, + {"assertions", json::array({ + { + {"label", "c2pa.actions.v2"}, + {"data", { + {"actions", json::array({ + { + {"action", "c2pa.placed"}, + {"parameters", { + {"ingredientIds", json::array({instance_id})} + }} + } + })} + }} + } + })} +}; + +c2pa::Builder builder(context, manifest_json.dump()); + +archive_file.seekg(0); +builder.add_ingredient( + json({ + {"title", ingredient["title"]}, + {"relationship", "componentOf"}, + {"instance_id", instance_id} + }).dump(), + "application/c2pa", + archive_file); + +builder.sign(source_path, output_path, signer); +``` + ### Merging multiple working stores In some cases you may need to merge ingredients from multiple working stores (builder archives) into a single `Builder`. This should be a **fallback strategy**—the recommended practice is to maintain a single active working store and add ingredients incrementally (archived ingredient catalogs help with this). Merging is available when multiple working stores must be consolidated.