Skip to content

Commit fca1a96

Browse files
author
Tania Mathern
committed
fix: Docs
1 parent 696ce95 commit fca1a96

1 file changed

Lines changed: 162 additions & 2 deletions

File tree

docs/selective-manifests.md

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,9 @@ with Reader("application/c2pa", archive_stream, context=ctx) as reader:
559559

560560
### Identifying ingredients in archives
561561

562-
When building an ingredient archive, you can set `instance_id` on the ingredient to give it a stable, caller-controlled identifier. This field survives archiving and signing unchanged. The `description` and `informational_URI` fields also survive and can carry additional metadata about the ingredient's origin.
562+
When building an ingredient archive, you can set `instance_id` on the ingredient to give it a stable, caller-controlled identifier. This field survives archiving and signing unchanged, so it can be used to look up a specific ingredient from a catalog archive. The `description` and `informational_URI` fields also survive and can carry additional metadata about the ingredient's origin.
563+
564+
`instance_id` is only for identification and catalog lookups. It cannot be used as a linking key in `ingredientIds` when linking ingredient archives to actions — use `label` for that (see [Linking an archived ingredient to an action](#linking-an-archived-ingredient-to-an-action)).
563565

564566
```py
565567
# Set instance_id when adding the ingredient to the archive builder
@@ -590,7 +592,7 @@ ingredients = manifest_data["manifests"][active]["ingredients"]
590592

591593
for ing in ingredients:
592594
if ing.get("instance_id") == "catalog:photo-A":
593-
# Found the target ingredient
595+
# Do something with the found ingredient...
594596
pass
595597
```
596598

@@ -768,6 +770,121 @@ with Reader("application/c2pa", archive_stream, context=ctx) as reader:
768770
new_builder.sign("image/jpeg", source, dest)
769771
```
770772

773+
### Reading ingredient details from an ingredient archive
774+
775+
An ingredient archive is a serialized `Builder` containing exactly 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.
776+
777+
```mermaid
778+
flowchart LR
779+
IA["ingredient_archive.c2pa"] -->|"Reader(application/c2pa)"| JSON["JSON + resources"]
780+
JSON --> TH["Thumbnail"]
781+
JSON --> AM["Active manifest?"]
782+
JSON --> VS["Validation status"]
783+
JSON --> REL["Relationship"]
784+
```
785+
786+
```py
787+
# Open the ingredient archive
788+
with open("ingredient_archive.c2pa", "rb") as archive_file:
789+
reader = Reader("application/c2pa", archive_file, context=ctx)
790+
parsed = json.loads(reader.json())
791+
active = parsed["active_manifest"]
792+
manifest = parsed["manifests"][active]
793+
794+
# An ingredient archive has exactly one ingredient
795+
ingredient = manifest["ingredients"][0]
796+
797+
# Relationship
798+
relationship = ingredient["relationship"] # e.g. "parentOf", "componentOf", "inputTo"
799+
800+
# Instance ID (optional, set by the caller via add_ingredient or derived from XMP metadata)
801+
instance_id = ingredient.get("instance_id")
802+
803+
# Active manifest:
804+
# When present, the ingredient was a signed asset and its manifest label
805+
# points into the top-level "manifests" dictionary.
806+
if "active_manifest" in ingredient:
807+
ing_manifest_label = ingredient["active_manifest"]
808+
ing_manifest = parsed["manifests"][ing_manifest_label]
809+
# ing_manifest contains the ingredient's own assertions, actions, etc.
810+
811+
# Validation status.
812+
# The top-level "validation_status" array covers the entire manifest store,
813+
# including this ingredient's manifest. An empty or absent array means
814+
# no validation errors were found.
815+
if "validation_status" in parsed:
816+
for status in parsed["validation_status"]:
817+
print(f"{status['code']}: {status['explanation']}")
818+
819+
# Thumbnail
820+
if "thumbnail" in ingredient:
821+
thumb_id = ingredient["thumbnail"]["identifier"]
822+
with open("thumbnail.jpg", "wb") as thumb_file:
823+
reader.resource_to_stream(thumb_id, thumb_file)
824+
825+
reader.close()
826+
```
827+
828+
#### Linking an archived ingredient to an action
829+
830+
After reading the ingredient details from an ingredient archive, the ingredient can be added to a new `Builder` and linked to an action. You must assign a `label` in the `add_ingredient()` call on the signing builder and use that label as the linking key in `ingredientIds`. Labels baked into the archive ingredient are not carried through, and `instance_id` does not work as a linking key for ingredient archives.
831+
832+
Labels are only used as build-time linking keys. The SDK may reassign the actual label in the signed manifest.
833+
834+
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`.
835+
836+
```py
837+
ctx = Context.from_dict({
838+
"builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},
839+
"signer": signer,
840+
})
841+
842+
# Read the ingredient archive
843+
with open("ingredient_archive.c2pa", "rb") as archive_file:
844+
reader = Reader("application/c2pa", archive_file, context=ctx)
845+
parsed = json.loads(reader.json())
846+
active = parsed["active_manifest"]
847+
ingredient = parsed["manifests"][active]["ingredients"][0]
848+
849+
# Use a caller-assigned label as the linking key
850+
manifest_json = {
851+
"claim_generator_info": [{"name": "an-application", "version": "0.1.0"}],
852+
"assertions": [
853+
{
854+
"label": "c2pa.actions.v2",
855+
"data": {
856+
"actions": [
857+
{
858+
"action": "c2pa.opened",
859+
"parameters": {
860+
"ingredientIds": ["archived-ingredient"]
861+
},
862+
}
863+
]
864+
},
865+
}
866+
],
867+
}
868+
869+
with Builder(manifest_json, context=ctx) as builder:
870+
# The label on the ingredient matches the entry in ingredientIds
871+
archive_file.seek(0)
872+
builder.add_ingredient(
873+
{
874+
"title": ingredient["title"],
875+
"relationship": "parentOf",
876+
"label": "archived-ingredient",
877+
},
878+
"application/c2pa",
879+
archive_file,
880+
)
881+
882+
with open("source.jpg", "rb") as source, open("output.jpg", "w+b") as dest:
883+
builder.sign("image/jpeg", source, dest)
884+
885+
reader.close()
886+
```
887+
771888
### Merging multiple working stores
772889

773890
> [!NOTE]
@@ -839,3 +956,46 @@ with Builder({
839956
# configure a dedicated Signer explicitly.
840957
builder.sign("image/jpeg", source, dest)
841958
```
959+
960+
## Controlling manifest embedding
961+
962+
By default, `sign()` embeds the manifest directly inside the output asset file.
963+
964+
### Remove the manifest from the asset entirely
965+
966+
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.):
967+
968+
```mermaid
969+
flowchart LR
970+
subgraph Default["Default (embedded)"]
971+
A1[Output Asset] --- A2[Image data + C2PA manifest]
972+
end
973+
974+
subgraph NoEmbed["With set_no_embed()"]
975+
B1[Output Asset] ~~~ B2[Manifest bytes with store as sidecar or uploaded to server]
976+
end
977+
```
978+
979+
```py
980+
ctx = Context.from_dict({
981+
"builder": {"claim_generator_info": {"name": "an-application", "version": "0.1.0"}},
982+
"signer": signer,
983+
})
984+
builder = Builder(manifest_json, context=ctx)
985+
builder.set_no_embed()
986+
builder.set_remote_url("<<URI/URL to remote storage of manifest bytes>>")
987+
988+
with open("source.jpg", "rb") as source, open("output.jpg", "w+b") as dest:
989+
manifest_bytes = builder.sign("image/jpeg", source, dest)
990+
# manifest_bytes contains the full manifest store
991+
# Upload manifest_bytes to the remote URL
992+
# The output asset has no embedded manifest
993+
```
994+
995+
Reading back:
996+
997+
```py
998+
reader = Reader("output.jpg", context=ctx)
999+
reader.is_embedded() # False
1000+
reader.remote_url() # "<<URI/URL to remote storage of manifest bytes>>"
1001+
```

0 commit comments

Comments
 (0)