@@ -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
44234495class TestStream (unittest .TestCase ):
44244496 def setUp (self ):
0 commit comments