@@ -3693,6 +3693,313 @@ def test_builder_sign_dicts_no_auto_add(self):
36933693 # Reset settings
36943694 load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true,"source_type":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}}}}' )
36953695
3696+ def test_builder_opened_action_one_ingredient_no_auto_add (self ):
3697+ """Test Builder with c2pa.opened action and one ingredient, following Adobe provenance patterns"""
3698+ # Disable auto-added actions
3699+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3700+
3701+ # Instance IDs for linking ingredients and actions
3702+ # This can be any unique id so the ingredient can be uniquely identified and linked to the action
3703+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
3704+
3705+ manifestDefinition = {
3706+ "claim_generator_info" : [{
3707+ "name" : "Python CAI test" ,
3708+ "version" : "3.14.16"
3709+ }],
3710+ "title" : "A title for the provenance test" ,
3711+ "ingredients" : [
3712+ # The parent ingredient will be added through add_ingredient
3713+ # And a properly crafted manifest json so they link
3714+ ],
3715+ "assertions" : [
3716+ {
3717+ "label" : "c2pa.actions.v2" ,
3718+ "data" : {
3719+ "actions" : [
3720+ {
3721+ "action" : "c2pa.opened" ,
3722+ "softwareAgent" : {
3723+ "name" : "Opened asset" ,
3724+ },
3725+ "parameters" : {
3726+ "ingredientIds" : [
3727+ parent_ingredient_id
3728+ ]
3729+ },
3730+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3731+ }
3732+ ]
3733+ }
3734+ }
3735+ ]
3736+ }
3737+
3738+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition
3739+ # Aka the unique parent_ingredient_id we rely on for linking
3740+ ingredient_json = {
3741+ "relationship" : "parentOf" ,
3742+ "instance_id" : parent_ingredient_id
3743+ }
3744+ # An opened ingredient is always a parent, and there can only be exactly one parent ingredient
3745+
3746+ # Read the input file (A.jpg will be signed)
3747+ with open (self .testPath2 , "rb" ) as test_file :
3748+ file_content = test_file .read ()
3749+
3750+ builder = Builder .from_json (manifestDefinition )
3751+
3752+ # Add C.jpg as the parent "opened" ingredient
3753+ with open (self .testPath , 'rb' ) as f :
3754+ builder .add_ingredient (ingredient_json , "image/jpeg" , f )
3755+
3756+ output_buffer = io .BytesIO (bytearray ())
3757+ builder .sign (
3758+ self .signer ,
3759+ "image/jpeg" ,
3760+ io .BytesIO (file_content ),
3761+ output_buffer )
3762+ output_buffer .seek (0 )
3763+
3764+ # Read and verify the manifest
3765+ reader = Reader ("image/jpeg" , output_buffer )
3766+ json_data = reader .json ()
3767+ manifest_data = json .loads (json_data )
3768+
3769+ # Verify the ingredient instance ID is present
3770+ self .assertIn (parent_ingredient_id , json_data )
3771+
3772+ # Verify c2pa.opened action is present
3773+ self .assertIn ("c2pa.opened" , json_data )
3774+
3775+ builder .close ()
3776+
3777+ # Make sure settings are put back to the common test defaults
3778+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3779+
3780+ def test_builder_one_opened_one_placed_action_no_auto_add (self ):
3781+ """Test Builder with c2pa.opened action where asset is its own parent ingredient"""
3782+ # Disable auto-added actions
3783+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3784+
3785+ # Instance IDs for linking ingredients and actions,
3786+ # need to be unique even if the same binary file is used, so ingredients link properly to actions
3787+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
3788+ placed_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
3789+
3790+ manifestDefinition = {
3791+ "claim_generator_info" : [{
3792+ "name" : "Python CAI test" ,
3793+ "version" : "0.2.942"
3794+ }],
3795+ "title" : "A title for the provenance test" ,
3796+ "ingredients" : [
3797+ # The parent ingredient will be added through add_ingredient
3798+ {
3799+ # Represents the bubbled up AI asset/ingredient
3800+ "format" : "jpeg" ,
3801+ "relationship" : "componentOf" ,
3802+ # Instance ID must be generated to match what is in parameters ingredientIds array
3803+ "instance_id" : placed_ingredient_id ,
3804+ }
3805+ ],
3806+ "assertions" : [
3807+ {
3808+ "label" : "c2pa.actions.v2" ,
3809+ "data" : {
3810+ "actions" : [
3811+ {
3812+ "action" : "c2pa.opened" ,
3813+ "softwareAgent" : {
3814+ "name" : "Opened asset" ,
3815+ },
3816+ "parameters" : {
3817+ "ingredientIds" : [
3818+ parent_ingredient_id
3819+ ]
3820+ },
3821+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3822+ },
3823+ {
3824+ "action" : "c2pa.placed" ,
3825+ "softwareAgent" : {
3826+ "name" : "Placed asset" ,
3827+ },
3828+ "parameters" : {
3829+ "ingredientIds" : [
3830+ placed_ingredient_id
3831+ ]
3832+ },
3833+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3834+ }
3835+ ]
3836+ }
3837+ }
3838+ ]
3839+ }
3840+
3841+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition for c2pa.opened
3842+ # So that ingredients can link together.
3843+ ingredient_json = {
3844+ "relationship" : "parentOf" ,
3845+ "when" : "2025-08-07T18:01:55.934Z" ,
3846+ "instance_id" : parent_ingredient_id
3847+ }
3848+
3849+ # Read the input file (A.jpg will be signed)
3850+ with open (self .testPath2 , "rb" ) as test_file :
3851+ file_content = test_file .read ()
3852+
3853+ builder = Builder .from_json (manifestDefinition )
3854+
3855+ # An asset can be its own parent ingredient!
3856+ # We add A.jpg as its own parent ingredient
3857+ with open (self .testPath2 , 'rb' ) as f :
3858+ builder .add_ingredient (ingredient_json , "image/jpeg" , f )
3859+
3860+ output_buffer = io .BytesIO (bytearray ())
3861+ builder .sign (
3862+ self .signer ,
3863+ "image/jpeg" ,
3864+ io .BytesIO (file_content ),
3865+ output_buffer )
3866+ output_buffer .seek (0 )
3867+
3868+ # Read and verify the manifest
3869+ reader = Reader ("image/jpeg" , output_buffer )
3870+ json_data = reader .json ()
3871+ manifest_data = json .loads (json_data )
3872+
3873+ # Verify both ingredient instance IDs are present
3874+ self .assertIn (parent_ingredient_id , json_data )
3875+ self .assertIn (placed_ingredient_id , json_data )
3876+
3877+ # Verify both actions are present
3878+ self .assertIn ("c2pa.opened" , json_data )
3879+ self .assertIn ("c2pa.placed" , json_data )
3880+
3881+ builder .close ()
3882+
3883+ # Make sure settings are put back to the common test defaults
3884+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3885+
3886+ def test_builder_opened_action_multiple_ingredient_no_auto_add (self ):
3887+ """Test Builder with c2pa.opened and c2pa.placed actions with multiple ingredients"""
3888+ # Disable auto-added actions, as what we are doing here can confuse auto-placements
3889+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3890+
3891+ # Instance IDs for linking ingredients and actions
3892+ # With multiple ingredients, we need multiple different unique ids so they each link properly
3893+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
3894+ placed_ingredient_1_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
3895+ placed_ingredient_2_id = "xmp:iid:a965983b-36fb-445a-aa80-f2d712acd14c"
3896+
3897+ manifestDefinition = {
3898+ "claim_generator_info" : [{
3899+ "name" : "Python CAI test" ,
3900+ "version" : "0.2.942"
3901+ }],
3902+ "title" : "A title for the provenance test with multiple ingredients" ,
3903+ "ingredients" : [
3904+ # More ingredients will be added using add_ingredient
3905+ {
3906+ "format" : "jpeg" ,
3907+ "relationship" : "componentOf" ,
3908+ # Instance ID must be generated to match what is in parameters ingredientIds array
3909+ "instance_id" : placed_ingredient_1_id ,
3910+ }
3911+ ],
3912+ "assertions" : [
3913+ {
3914+ "label" : "c2pa.actions.v2" ,
3915+ "data" : {
3916+ "actions" : [
3917+ {
3918+ "action" : "c2pa.opened" ,
3919+ "softwareAgent" : {
3920+ "name" : "A parent opened asset" ,
3921+ },
3922+ "parameters" : {
3923+ "ingredientIds" : [
3924+ parent_ingredient_id
3925+ ]
3926+ },
3927+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3928+ },
3929+ {
3930+ "action" : "c2pa.placed" ,
3931+ "softwareAgent" : {
3932+ "name" : "Component placed assets" ,
3933+ },
3934+ "parameters" : {
3935+ "ingredientIds" : [
3936+ placed_ingredient_1_id ,
3937+ placed_ingredient_2_id
3938+ ]
3939+ },
3940+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3941+ }
3942+ ]
3943+ }
3944+ }
3945+ ]
3946+ }
3947+
3948+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition,
3949+ # so that ingredients properly link with their action
3950+ ingredient_json_parent = {
3951+ "relationship" : "parentOf" ,
3952+ "instance_id" : parent_ingredient_id
3953+ }
3954+
3955+ # The ingredient json for the placed action needs to match the instance_id in the manifestDefinition,
3956+ # so that ingredients properly link with their action
3957+ ingredient_json_placed = {
3958+ "relationship" : "componentOf" ,
3959+ "instance_id" : placed_ingredient_2_id
3960+ }
3961+
3962+ # Read the input file (A.jpg will be signed)
3963+ with open (self .testPath2 , "rb" ) as test_file :
3964+ file_content = test_file .read ()
3965+
3966+ builder = Builder .from_json (manifestDefinition )
3967+
3968+ # Add C.jpg as the parent ingredient (for c2pa.opened, it's the opened asset)
3969+ with open (self .testPath , 'rb' ) as f1 :
3970+ builder .add_ingredient (ingredient_json_parent , "image/jpeg" , f1 )
3971+
3972+ # Add cloud.jpg as another placed ingredient (for instance, added on the opened asset)
3973+ with open (self .testPath4 , 'rb' ) as f2 :
3974+ builder .add_ingredient (ingredient_json_placed , "image/jpeg" , f2 )
3975+
3976+ output_buffer = io .BytesIO (bytearray ())
3977+ builder .sign (
3978+ self .signer ,
3979+ "image/jpeg" ,
3980+ io .BytesIO (file_content ),
3981+ output_buffer )
3982+ output_buffer .seek (0 )
3983+
3984+ # Read and verify the manifest
3985+ reader = Reader ("image/jpeg" , output_buffer )
3986+ json_data = reader .json ()
3987+ manifest_data = json .loads (json_data )
3988+
3989+ # Verify all ingredient instance IDs are present
3990+ self .assertIn (parent_ingredient_id , json_data )
3991+ self .assertIn (placed_ingredient_1_id , json_data )
3992+ self .assertIn (placed_ingredient_2_id , json_data )
3993+
3994+ # Verify both actions are present
3995+ self .assertIn ("c2pa.opened" , json_data )
3996+ self .assertIn ("c2pa.placed" , json_data )
3997+
3998+ builder .close ()
3999+
4000+ # Make sure settings are put back to the common test defaults
4001+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
4002+
36964003
36974004class TestStream (unittest .TestCase ):
36984005 def setUp (self ):
0 commit comments