Skip to content

Commit ef77a0b

Browse files
author
Tania Mathern
committed
fix: Docs initial cleanup
1 parent f6b0e07 commit ef77a0b

2 files changed

Lines changed: 169 additions & 39 deletions

File tree

docs/working-stores.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,9 +492,65 @@ with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst:
492492
builder.sign("image/jpeg", src, dst)
493493
```
494494

495-
When linking multiple ingredient archives, give each a distinct label and reference them separately in `ingredientIds`:
495+
When linking multiple ingredient archives, give each a distinct label and reference it in the appropriate action's `ingredientIds` array.
496+
497+
If each ingredient has its own action (e.g., one `c2pa.opened` for the parent and one `c2pa.placed` for a composited element), set up two actions with separate `ingredientIds`:
496498

497499
```py
500+
manifest_json = {
501+
"claim_generator_info": [{"name": "my-app", "version": "1.0"}],
502+
"assertions": [{
503+
"label": "c2pa.actions.v2",
504+
"data": {
505+
"actions": [
506+
{
507+
"action": "c2pa.opened",
508+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
509+
"parameters": {"ingredientIds": ["parent-photo"]},
510+
},
511+
{
512+
"action": "c2pa.placed",
513+
"parameters": {"ingredientIds": ["overlay-graphic"]},
514+
},
515+
]
516+
},
517+
}],
518+
}
519+
520+
builder = Builder(manifest_json, context=ctx)
521+
522+
builder.add_ingredient(
523+
{"title": "photo.jpg", "relationship": "parentOf", "label": "parent-photo"},
524+
"application/c2pa",
525+
photo_archive,
526+
)
527+
builder.add_ingredient(
528+
{"title": "overlay.png", "relationship": "componentOf", "label": "overlay-graphic"},
529+
"application/c2pa",
530+
overlay_archive,
531+
)
532+
```
533+
534+
A single `c2pa.placed` action can also reference several `componentOf` ingredients composited together. List all labels in the `ingredientIds` array:
535+
536+
```py
537+
manifest_json = {
538+
"claim_generator_info": [{"name": "my-app", "version": "1.0"}],
539+
"assertions": [{
540+
"label": "c2pa.actions.v2",
541+
"data": {
542+
"actions": [{
543+
"action": "c2pa.placed",
544+
"parameters": {
545+
"ingredientIds": ["base-layer", "overlay-layer"]
546+
},
547+
}]
548+
},
549+
}],
550+
}
551+
552+
builder = Builder(manifest_json, context=ctx)
553+
498554
builder.add_ingredient(
499555
{"title": "base.jpg", "relationship": "componentOf", "label": "base-layer"},
500556
"application/c2pa",
@@ -507,6 +563,8 @@ builder.add_ingredient(
507563
)
508564
```
509565

566+
After signing, the action's `parameters.ingredients` array contains one resolved URL per ingredient.
567+
510568
### Ingredient relationships
511569

512570
Specify the relationship between the ingredient and the current asset:

tests/test_unit_tests.py

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,35 @@ def callback_signer_es256(data: bytes) -> bytes:
10211021
return signature
10221022
self.callback_signer_es256 = callback_signer_es256
10231023

1024+
def _create_ingredient_archive(self, ingredient_json=None):
1025+
"""Helper: create an ingredient archive from a single ingredient."""
1026+
if ingredient_json is None:
1027+
ingredient_json = {"title": "photo.jpg", "relationship": "componentOf"}
1028+
manifest = {
1029+
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
1030+
"assertions": [
1031+
{
1032+
"label": "c2pa.actions",
1033+
"data": {
1034+
"actions": [
1035+
{
1036+
"action": "c2pa.created",
1037+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
1038+
}
1039+
]
1040+
},
1041+
}
1042+
],
1043+
}
1044+
builder = Builder.from_json(manifest)
1045+
with open(self.testPath, "rb") as f:
1046+
builder.add_ingredient(ingredient_json, "image/jpeg", f)
1047+
archive = io.BytesIO()
1048+
builder.to_archive(archive)
1049+
builder.close()
1050+
archive.seek(0)
1051+
return archive
1052+
10241053
def test_can_retrieve_builder_supported_mimetypes(self):
10251054
result1 = Builder.get_supported_mime_types()
10261055
self.assertTrue(len(result1) > 0)
@@ -4151,44 +4180,6 @@ def test_builder_opened_action_multiple_ingredient_no_auto_add(self):
41514180
# Make sure settings are put back to the common test defaults
41524181
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
41534182

4154-
4155-
# -----------------------------------------------------------------------
4156-
# Tests: Linking ingredient archives to actions.
4157-
#
4158-
# Only labels set on the signing builder's add_ingredient call work for
4159-
# linking ingredient archives to actions via ingredientIds.
4160-
# Labels baked into the archive and instance_id (anywhere) do NOT work.
4161-
# -----------------------------------------------------------------------
4162-
4163-
def _create_ingredient_archive(self, ingredient_json=None):
4164-
"""Helper: create an ingredient archive from a single ingredient."""
4165-
if ingredient_json is None:
4166-
ingredient_json = {"title": "photo.jpg", "relationship": "componentOf"}
4167-
manifest = {
4168-
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4169-
"assertions": [
4170-
{
4171-
"label": "c2pa.actions",
4172-
"data": {
4173-
"actions": [
4174-
{
4175-
"action": "c2pa.created",
4176-
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4177-
}
4178-
]
4179-
},
4180-
}
4181-
],
4182-
}
4183-
builder = Builder.from_json(manifest)
4184-
with open(self.testPath, "rb") as f:
4185-
builder.add_ingredient(ingredient_json, "image/jpeg", f)
4186-
archive = io.BytesIO()
4187-
builder.to_archive(archive)
4188-
builder.close()
4189-
archive.seek(0)
4190-
return archive
4191-
41924183
def test_link_archive_label_on_signing_builder_placed(self):
41934184
"""Label set on the signing builder's add_ingredient links an
41944185
ingredient archive to a c2pa.placed action."""
@@ -4419,6 +4410,87 @@ def test_link_archive_two_ingredients_labels(self):
44194410

44204411
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
44214412

4413+
def test_link_archive_multiple_ingredients_in_one_placed_action(self):
4414+
"""A single c2pa.placed action references two componentOf ingredients
4415+
via ingredientIds with two labels."""
4416+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4417+
4418+
archive1 = self._create_ingredient_archive(
4419+
{"title": "base-layer.jpg", "relationship": "componentOf"}
4420+
)
4421+
archive2 = self._create_ingredient_archive(
4422+
{"title": "overlay-layer.jpg", "relationship": "componentOf"}
4423+
)
4424+
4425+
manifest = {
4426+
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4427+
"assertions": [
4428+
{
4429+
"label": "c2pa.actions.v2",
4430+
"data": {
4431+
"actions": [
4432+
{
4433+
"action": "c2pa.placed",
4434+
"parameters": {
4435+
"ingredientIds": ["base-layer", "overlay-layer"]
4436+
},
4437+
}
4438+
]
4439+
},
4440+
}
4441+
],
4442+
}
4443+
4444+
builder = Builder.from_json(manifest)
4445+
builder.add_ingredient(
4446+
{"title": "base-layer.jpg", "relationship": "componentOf", "label": "base-layer"},
4447+
"application/c2pa",
4448+
archive1,
4449+
)
4450+
builder.add_ingredient(
4451+
{"title": "overlay-layer.jpg", "relationship": "componentOf", "label": "overlay-layer"},
4452+
"application/c2pa",
4453+
archive2,
4454+
)
4455+
4456+
with open(self.testPath, "rb") as src:
4457+
output = io.BytesIO()
4458+
builder.sign(self.signer, "image/jpeg", src, output)
4459+
output.seek(0)
4460+
4461+
reader = Reader("image/jpeg", output)
4462+
manifest_data = json.loads(reader.json())
4463+
active = manifest_data["active_manifest"]
4464+
assertions = manifest_data["manifests"][active]["assertions"]
4465+
4466+
placed_action = None
4467+
for assertion in assertions:
4468+
if assertion.get("label") == "c2pa.actions.v2":
4469+
for action in assertion["data"]["actions"]:
4470+
if action["action"] == "c2pa.placed":
4471+
placed_action = action
4472+
break
4473+
4474+
self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4475+
self.assertIn("parameters", placed_action)
4476+
self.assertIn("ingredients", placed_action["parameters"])
4477+
ingredients = placed_action["parameters"]["ingredients"]
4478+
self.assertEqual(len(ingredients), 2,
4479+
"c2pa.placed should reference both ingredients")
4480+
4481+
url0 = ingredients[0]["url"]
4482+
url1 = ingredients[1]["url"]
4483+
self.assertNotEqual(url0, url1,
4484+
"Each ingredient should have a distinct URL")
4485+
4486+
reader.close()
4487+
output.close()
4488+
archive1.close()
4489+
archive2.close()
4490+
builder.close()
4491+
4492+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4493+
44224494

44234495
class TestStream(unittest.TestCase):
44244496
def setUp(self):

0 commit comments

Comments
 (0)