Skip to content

Commit 2a56ac0

Browse files
tmathernTania Mathern
andauthored
fix: More docs (#175)
Co-authored-by: Tania Mathern <tania.mathern@gmail.comn>
1 parent 71d52fa commit 2a56ac0

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

docs/selective-manifests.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,174 @@ for (auto& ingredient : selected) {
683683
new_builder.sign(source_path, output_path, signer);
684684
```
685685
686+
### Reading ingredient details from an ingredient archive
687+
688+
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.
689+
690+
```mermaid
691+
flowchart LR
692+
IA["ingredient_archive.c2pa"] -->|"Reader(application/c2pa)"| JSON["JSON + resources"]
693+
JSON --> TH["Thumbnail"]
694+
JSON --> AM["Active manifest?"]
695+
JSON --> VS["Validation status"]
696+
JSON --> REL["Relationship"]
697+
```
698+
699+
```cpp
700+
// Open the ingredient archive
701+
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
702+
c2pa::Reader reader(context, "application/c2pa", archive_file);
703+
auto parsed = json::parse(reader.json());
704+
std::string active = parsed["active_manifest"];
705+
auto manifest = parsed["manifests"][active];
706+
707+
// An ingredient archive has exactly one ingredient
708+
auto& ingredient = manifest["ingredients"][0];
709+
710+
// Relationship
711+
std::string relationship = ingredient["relationship"]; // e.g. "parentOf", "componentOf", "inputTo"
712+
713+
// Instance ID (optional, set by the caller via add_ingredient or derived from XMP metadata)
714+
std::string instance_id;
715+
if (ingredient.contains("instance_id")) {
716+
instance_id = ingredient["instance_id"];
717+
}
718+
719+
// Active manifest:
720+
// When present, the ingredient was a signed asset and its manifest label
721+
// points into the top-level "manifests" dictionary.
722+
bool has_provenance = ingredient.contains("active_manifest");
723+
if (has_provenance) {
724+
std::string ing_manifest_label = ingredient["active_manifest"];
725+
auto ing_manifest = parsed["manifests"][ing_manifest_label];
726+
// ing_manifest contains the ingredient's own assertions, actions, etc.
727+
}
728+
729+
// Validation status.
730+
// The top-level "validation_status" array covers the entire manifest store,
731+
// including this ingredient's manifest. An empty or absent array means
732+
// no validation errors were found.
733+
if (parsed.contains("validation_status")) {
734+
for (auto& status : parsed["validation_status"]) {
735+
std::cout << status["code"].get<std::string>() << ": "
736+
<< status["explanation"].get<std::string>() << std::endl;
737+
}
738+
}
739+
740+
// Thumbnail
741+
if (ingredient.contains("thumbnail")) {
742+
std::string thumb_id = ingredient["thumbnail"]["identifier"];
743+
std::stringstream thumb_stream(std::ios::in | std::ios::out | std::ios::binary);
744+
reader.get_resource(thumb_id, thumb_stream);
745+
// thumb_stream now contains the thumbnail binary data
746+
}
747+
```
748+
749+
#### Linking an archived ingredient to an action
750+
751+
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.
752+
753+
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.
754+
755+
##### Using a label
756+
757+
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`.
758+
759+
```cpp
760+
c2pa::Context context;
761+
762+
// Read the ingredient archive
763+
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
764+
c2pa::Reader reader(context, "application/c2pa", archive_file);
765+
auto parsed = json::parse(reader.json());
766+
std::string active = parsed["active_manifest"];
767+
auto& ingredient = parsed["manifests"][active]["ingredients"][0];
768+
769+
// Use a caller-assigned label as the linking key
770+
json manifest_json = {
771+
{"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})},
772+
{"assertions", json::array({
773+
{
774+
{"label", "c2pa.actions.v2"},
775+
{"data", {
776+
{"actions", json::array({
777+
{
778+
{"action", "c2pa.opened"},
779+
{"parameters", {
780+
{"ingredientIds", json::array({"archived-ingredient"})}
781+
}}
782+
}
783+
})}
784+
}}
785+
}
786+
})}
787+
};
788+
789+
c2pa::Builder builder(context, manifest_json.dump());
790+
791+
// The label on the ingredient JSON matches the entry in ingredientIds
792+
archive_file.seekg(0);
793+
builder.add_ingredient(
794+
json({
795+
{"title", ingredient["title"]},
796+
{"relationship", "parentOf"},
797+
{"label", "archived-ingredient"}
798+
}).dump(),
799+
"application/c2pa",
800+
archive_file);
801+
802+
builder.sign(source_path, output_path, signer);
803+
```
804+
805+
##### Using an `instance_id`
806+
807+
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.
808+
809+
```cpp
810+
c2pa::Context context;
811+
812+
// Read the ingredient archive and extract the instance_id
813+
std::ifstream archive_file("ingredient_archive.c2pa", std::ios::binary);
814+
c2pa::Reader reader(context, "application/c2pa", archive_file);
815+
auto parsed = json::parse(reader.json());
816+
std::string active = parsed["active_manifest"];
817+
auto& ingredient = parsed["manifests"][active]["ingredients"][0];
818+
std::string instance_id = ingredient["instance_id"];
819+
820+
json manifest_json = {
821+
{"claim_generator_info", json::array({{{"name", "an-application"}, {"version", "1.0"}}})},
822+
{"assertions", json::array({
823+
{
824+
{"label", "c2pa.actions.v2"},
825+
{"data", {
826+
{"actions", json::array({
827+
{
828+
{"action", "c2pa.placed"},
829+
{"parameters", {
830+
{"ingredientIds", json::array({instance_id})}
831+
}}
832+
}
833+
})}
834+
}}
835+
}
836+
})}
837+
};
838+
839+
c2pa::Builder builder(context, manifest_json.dump());
840+
841+
archive_file.seekg(0);
842+
builder.add_ingredient(
843+
json({
844+
{"title", ingredient["title"]},
845+
{"relationship", "componentOf"},
846+
{"instance_id", instance_id}
847+
}).dump(),
848+
"application/c2pa",
849+
archive_file);
850+
851+
builder.sign(source_path, output_path, signer);
852+
```
853+
686854
### Merging multiple working stores
687855
688856
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.

0 commit comments

Comments
 (0)