Skip to content

Commit f6b0e07

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

2 files changed

Lines changed: 340 additions & 0 deletions

File tree

docs/working-stores.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,78 @@ with open("new_asset.jpg", "rb") as src, open("signed_asset.jpg", "w+b") as dst:
435435
builder.sign("image/jpeg", src, dst)
436436
```
437437

438+
### Linking an ingredient archive to an action
439+
440+
To link an ingredient archive to an action via `ingredientIds`, you must use a `label` set in the `add_ingredient()` call on the signing builder. Labels baked into the archive ingredient are not carried through, and `instance_id` does not work as a linking key for ingredient archives regardless of where it is set.
441+
442+
```py
443+
import io, json
444+
445+
# Step 1: Create the ingredient archive
446+
archive_builder = Builder.from_json({
447+
"claim_generator_info": [{"name": "my-app", "version": "1.0"}],
448+
"assertions": [],
449+
})
450+
with open("photo.jpg", "rb") as f:
451+
archive_builder.add_ingredient(
452+
{"title": "photo.jpg", "relationship": "componentOf"},
453+
"image/jpeg",
454+
f,
455+
)
456+
archive = io.BytesIO()
457+
archive_builder.to_archive(archive)
458+
archive.seek(0)
459+
460+
# Step 2: Build a manifest with an action that references the ingredient
461+
manifest_json = {
462+
"claim_generator_info": [{"name": "my-app", "version": "1.0"}],
463+
"assertions": [
464+
{
465+
"label": "c2pa.actions.v2",
466+
"data": {
467+
"actions": [
468+
{
469+
"action": "c2pa.placed",
470+
"parameters": {
471+
"ingredientIds": ["my-ingredient"]
472+
},
473+
}
474+
]
475+
},
476+
}
477+
],
478+
}
479+
480+
ctx = Context.from_dict({"signer": signer})
481+
builder = Builder(manifest_json, context=ctx)
482+
483+
# Step 3: Add the ingredient archive with a label matching the ingredientIds value.
484+
# The label MUST be set here, on the signing builder's add_ingredient call.
485+
builder.add_ingredient(
486+
{"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"},
487+
"application/c2pa",
488+
archive,
489+
)
490+
491+
with open("source.jpg", "rb") as src, open("signed.jpg", "w+b") as dst:
492+
builder.sign("image/jpeg", src, dst)
493+
```
494+
495+
When linking multiple ingredient archives, give each a distinct label and reference them separately in `ingredientIds`:
496+
497+
```py
498+
builder.add_ingredient(
499+
{"title": "base.jpg", "relationship": "componentOf", "label": "base-layer"},
500+
"application/c2pa",
501+
base_archive,
502+
)
503+
builder.add_ingredient(
504+
{"title": "overlay.jpg", "relationship": "componentOf", "label": "overlay-layer"},
505+
"application/c2pa",
506+
overlay_archive,
507+
)
508+
```
509+
438510
### Ingredient relationships
439511

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

tests/test_unit_tests.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4152,6 +4152,274 @@ def test_builder_opened_action_multiple_ingredient_no_auto_add(self):
41524152
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
41534153

41544154

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+
4192+
def test_link_archive_label_on_signing_builder_placed(self):
4193+
"""Label set on the signing builder's add_ingredient links an
4194+
ingredient archive to a c2pa.placed action."""
4195+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4196+
4197+
archive = self._create_ingredient_archive()
4198+
4199+
manifest = {
4200+
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4201+
"assertions": [
4202+
{
4203+
"label": "c2pa.actions.v2",
4204+
"data": {
4205+
"actions": [
4206+
{
4207+
"action": "c2pa.placed",
4208+
"parameters": {
4209+
"ingredientIds": ["my-ingredient"]
4210+
},
4211+
}
4212+
]
4213+
},
4214+
}
4215+
],
4216+
}
4217+
4218+
builder = Builder.from_json(manifest)
4219+
builder.add_ingredient(
4220+
{"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"},
4221+
"application/c2pa",
4222+
archive,
4223+
)
4224+
4225+
with open(self.testPath, "rb") as src:
4226+
output = io.BytesIO()
4227+
builder.sign(self.signer, "image/jpeg", src, output)
4228+
output.seek(0)
4229+
4230+
reader = Reader("image/jpeg", output)
4231+
manifest_data = json.loads(reader.json())
4232+
active = manifest_data["active_manifest"]
4233+
assertions = manifest_data["manifests"][active]["assertions"]
4234+
4235+
placed_action = None
4236+
for assertion in assertions:
4237+
if assertion.get("label") == "c2pa.actions.v2":
4238+
for action in assertion["data"]["actions"]:
4239+
if action["action"] == "c2pa.placed":
4240+
placed_action = action
4241+
break
4242+
4243+
self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4244+
self.assertIn("parameters", placed_action)
4245+
self.assertIn("ingredients", placed_action["parameters"])
4246+
self.assertEqual(len(placed_action["parameters"]["ingredients"]), 1)
4247+
self.assertIn(
4248+
"c2pa.ingredient.v3",
4249+
placed_action["parameters"]["ingredients"][0]["url"],
4250+
)
4251+
4252+
reader.close()
4253+
output.close()
4254+
archive.close()
4255+
builder.close()
4256+
4257+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4258+
4259+
def test_link_archive_label_on_signing_builder_opened(self):
4260+
"""Label set on the signing builder's add_ingredient links an
4261+
ingredient archive to a c2pa.opened action."""
4262+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4263+
4264+
archive = self._create_ingredient_archive(
4265+
{"title": "photo.jpg", "relationship": "parentOf"}
4266+
)
4267+
4268+
manifest = {
4269+
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4270+
"assertions": [
4271+
{
4272+
"label": "c2pa.actions.v2",
4273+
"data": {
4274+
"actions": [
4275+
{
4276+
"action": "c2pa.opened",
4277+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4278+
"parameters": {
4279+
"ingredientIds": ["my-ingredient"]
4280+
},
4281+
}
4282+
]
4283+
},
4284+
}
4285+
],
4286+
}
4287+
4288+
builder = Builder.from_json(manifest)
4289+
builder.add_ingredient(
4290+
{"title": "photo.jpg", "relationship": "parentOf", "label": "my-ingredient"},
4291+
"application/c2pa",
4292+
archive,
4293+
)
4294+
4295+
with open(self.testPath, "rb") as src:
4296+
output = io.BytesIO()
4297+
builder.sign(self.signer, "image/jpeg", src, output)
4298+
output.seek(0)
4299+
4300+
reader = Reader("image/jpeg", output)
4301+
manifest_data = json.loads(reader.json())
4302+
active = manifest_data["active_manifest"]
4303+
assertions = manifest_data["manifests"][active]["assertions"]
4304+
4305+
opened_action = None
4306+
for assertion in assertions:
4307+
if assertion.get("label") == "c2pa.actions.v2":
4308+
for action in assertion["data"]["actions"]:
4309+
if action["action"] == "c2pa.opened":
4310+
opened_action = action
4311+
break
4312+
4313+
self.assertIsNotNone(opened_action, "c2pa.opened action not found")
4314+
self.assertIn("parameters", opened_action)
4315+
self.assertIn("ingredients", opened_action["parameters"])
4316+
self.assertEqual(len(opened_action["parameters"]["ingredients"]), 1)
4317+
self.assertIn(
4318+
"c2pa.ingredient.v3",
4319+
opened_action["parameters"]["ingredients"][0]["url"],
4320+
)
4321+
4322+
reader.close()
4323+
output.close()
4324+
archive.close()
4325+
builder.close()
4326+
4327+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4328+
4329+
def test_link_archive_two_ingredients_labels(self):
4330+
"""Two ingredient archives linked to two different actions via
4331+
distinct labels. Verifies no cross-linking."""
4332+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4333+
4334+
archive1 = self._create_ingredient_archive(
4335+
{"title": "photo-placed.jpg", "relationship": "componentOf"}
4336+
)
4337+
archive2 = self._create_ingredient_archive(
4338+
{"title": "photo-opened.jpg", "relationship": "parentOf"}
4339+
)
4340+
4341+
manifest = {
4342+
"claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4343+
"assertions": [
4344+
{
4345+
"label": "c2pa.actions.v2",
4346+
"data": {
4347+
"actions": [
4348+
{
4349+
"action": "c2pa.placed",
4350+
"parameters": {
4351+
"ingredientIds": ["ingredient-for-placed"]
4352+
},
4353+
},
4354+
{
4355+
"action": "c2pa.opened",
4356+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4357+
"parameters": {
4358+
"ingredientIds": ["ingredient-for-opened"]
4359+
},
4360+
},
4361+
]
4362+
},
4363+
}
4364+
],
4365+
}
4366+
4367+
builder = Builder.from_json(manifest)
4368+
builder.add_ingredient(
4369+
{"title": "photo-placed.jpg", "relationship": "componentOf", "label": "ingredient-for-placed"},
4370+
"application/c2pa",
4371+
archive1,
4372+
)
4373+
builder.add_ingredient(
4374+
{"title": "photo-opened.jpg", "relationship": "parentOf", "label": "ingredient-for-opened"},
4375+
"application/c2pa",
4376+
archive2,
4377+
)
4378+
4379+
with open(self.testPath, "rb") as src:
4380+
output = io.BytesIO()
4381+
builder.sign(self.signer, "image/jpeg", src, output)
4382+
output.seek(0)
4383+
4384+
reader = Reader("image/jpeg", output)
4385+
manifest_data = json.loads(reader.json())
4386+
active = manifest_data["active_manifest"]
4387+
assertions = manifest_data["manifests"][active]["assertions"]
4388+
4389+
placed_action = None
4390+
opened_action = None
4391+
for assertion in assertions:
4392+
if assertion.get("label") == "c2pa.actions.v2":
4393+
for action in assertion["data"]["actions"]:
4394+
if action["action"] == "c2pa.placed":
4395+
placed_action = action
4396+
if action["action"] == "c2pa.opened":
4397+
opened_action = action
4398+
4399+
self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4400+
self.assertIsNotNone(opened_action, "c2pa.opened action not found")
4401+
4402+
self.assertIn("ingredients", placed_action["parameters"])
4403+
self.assertEqual(len(placed_action["parameters"]["ingredients"]), 1)
4404+
placed_url = placed_action["parameters"]["ingredients"][0]["url"]
4405+
4406+
self.assertIn("ingredients", opened_action["parameters"])
4407+
self.assertEqual(len(opened_action["parameters"]["ingredients"]), 1)
4408+
opened_url = opened_action["parameters"]["ingredients"][0]["url"]
4409+
4410+
# Each action should link to a different ingredient (no cross-linking)
4411+
self.assertNotEqual(placed_url, opened_url,
4412+
"Each action should link to a different ingredient")
4413+
4414+
reader.close()
4415+
output.close()
4416+
archive1.close()
4417+
archive2.close()
4418+
builder.close()
4419+
4420+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4421+
4422+
41554423
class TestStream(unittest.TestCase):
41564424
def setUp(self):
41574425
self.temp_file = io.BytesIO()

0 commit comments

Comments
 (0)