@@ -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+
41554423class TestStream (unittest .TestCase ):
41564424 def setUp (self ):
41574425 self .temp_file = io .BytesIO ()
0 commit comments