@@ -2924,6 +2924,313 @@ def test_builder_sign_dicts_no_auto_add(self):
29242924 # Reset settings
29252925 load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}' )
29262926
2927+ def test_builder_opened_action_one_ingredient_no_auto_add (self ):
2928+ """Test Builder with c2pa.opened action and one ingredient, following Adobe provenance patterns"""
2929+ # Disable auto-added actions
2930+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
2931+
2932+ # Instance IDs for linking ingredients and actions
2933+ # This can be any unique id so the ingredient can be uniquely identified and linked to the action
2934+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
2935+
2936+ manifestDefinition = {
2937+ "claim_generator_info" : [{
2938+ "name" : "Python CAI test" ,
2939+ "version" : "3.14.16"
2940+ }],
2941+ "title" : "A title for the provenance test" ,
2942+ "ingredients" : [
2943+ # The parent ingredient will be added through add_ingredient
2944+ # And a properly crafted manifest json so they link
2945+ ],
2946+ "assertions" : [
2947+ {
2948+ "label" : "c2pa.actions.v2" ,
2949+ "data" : {
2950+ "actions" : [
2951+ {
2952+ "action" : "c2pa.opened" ,
2953+ "softwareAgent" : {
2954+ "name" : "Opened asset" ,
2955+ },
2956+ "parameters" : {
2957+ "ingredientIds" : [
2958+ parent_ingredient_id
2959+ ]
2960+ },
2961+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
2962+ }
2963+ ]
2964+ }
2965+ }
2966+ ]
2967+ }
2968+
2969+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition
2970+ # Aka the unique parent_ingredient_id we rely on for linking
2971+ ingredient_json = {
2972+ "relationship" : "parentOf" ,
2973+ "instance_id" : parent_ingredient_id
2974+ }
2975+ # An opened ingredient is always a parent, and there can only be exactly one parent ingredient
2976+
2977+ # Read the input file (A.jpg will be signed)
2978+ with open (self .testPath2 , "rb" ) as test_file :
2979+ file_content = test_file .read ()
2980+
2981+ builder = Builder .from_json (manifestDefinition )
2982+
2983+ # Add C.jpg as the parent "opened" ingredient
2984+ with open (self .testPath , 'rb' ) as f :
2985+ builder .add_ingredient (ingredient_json , "image/jpeg" , f )
2986+
2987+ output_buffer = io .BytesIO (bytearray ())
2988+ builder .sign (
2989+ self .signer ,
2990+ "image/jpeg" ,
2991+ io .BytesIO (file_content ),
2992+ output_buffer )
2993+ output_buffer .seek (0 )
2994+
2995+ # Read and verify the manifest
2996+ reader = Reader ("image/jpeg" , output_buffer )
2997+ json_data = reader .json ()
2998+ manifest_data = json .loads (json_data )
2999+
3000+ # Verify the ingredient instance ID is present
3001+ self .assertIn (parent_ingredient_id , json_data )
3002+
3003+ # Verify c2pa.opened action is present
3004+ self .assertIn ("c2pa.opened" , json_data )
3005+
3006+ builder .close ()
3007+
3008+ # Make sure settings are put back to the common test defaults
3009+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3010+
3011+ def test_builder_one_opened_one_placed_action_no_auto_add (self ):
3012+ """Test Builder with c2pa.opened action where asset is its own parent ingredient"""
3013+ # Disable auto-added actions
3014+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3015+
3016+ # Instance IDs for linking ingredients and actions,
3017+ # need to be unique even if the same binary file is used, so ingredients link properly to actions
3018+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
3019+ placed_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
3020+
3021+ manifestDefinition = {
3022+ "claim_generator_info" : [{
3023+ "name" : "Python CAI test" ,
3024+ "version" : "0.2.942"
3025+ }],
3026+ "title" : "A title for the provenance test" ,
3027+ "ingredients" : [
3028+ # The parent ingredient will be added through add_ingredient
3029+ {
3030+ # Represents the bubbled up AI asset/ingredient
3031+ "format" : "jpeg" ,
3032+ "relationship" : "componentOf" ,
3033+ # Instance ID must be generated to match what is in parameters ingredientIds array
3034+ "instance_id" : placed_ingredient_id ,
3035+ }
3036+ ],
3037+ "assertions" : [
3038+ {
3039+ "label" : "c2pa.actions.v2" ,
3040+ "data" : {
3041+ "actions" : [
3042+ {
3043+ "action" : "c2pa.opened" ,
3044+ "softwareAgent" : {
3045+ "name" : "Opened asset" ,
3046+ },
3047+ "parameters" : {
3048+ "ingredientIds" : [
3049+ parent_ingredient_id
3050+ ]
3051+ },
3052+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3053+ },
3054+ {
3055+ "action" : "c2pa.placed" ,
3056+ "softwareAgent" : {
3057+ "name" : "Placed asset" ,
3058+ },
3059+ "parameters" : {
3060+ "ingredientIds" : [
3061+ placed_ingredient_id
3062+ ]
3063+ },
3064+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3065+ }
3066+ ]
3067+ }
3068+ }
3069+ ]
3070+ }
3071+
3072+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition for c2pa.opened
3073+ # So that ingredients can link together.
3074+ ingredient_json = {
3075+ "relationship" : "parentOf" ,
3076+ "when" : "2025-08-07T18:01:55.934Z" ,
3077+ "instance_id" : parent_ingredient_id
3078+ }
3079+
3080+ # Read the input file (A.jpg will be signed)
3081+ with open (self .testPath2 , "rb" ) as test_file :
3082+ file_content = test_file .read ()
3083+
3084+ builder = Builder .from_json (manifestDefinition )
3085+
3086+ # An asset can be its own parent ingredient!
3087+ # We add A.jpg as its own parent ingredient
3088+ with open (self .testPath2 , 'rb' ) as f :
3089+ builder .add_ingredient (ingredient_json , "image/jpeg" , f )
3090+
3091+ output_buffer = io .BytesIO (bytearray ())
3092+ builder .sign (
3093+ self .signer ,
3094+ "image/jpeg" ,
3095+ io .BytesIO (file_content ),
3096+ output_buffer )
3097+ output_buffer .seek (0 )
3098+
3099+ # Read and verify the manifest
3100+ reader = Reader ("image/jpeg" , output_buffer )
3101+ json_data = reader .json ()
3102+ manifest_data = json .loads (json_data )
3103+
3104+ # Verify both ingredient instance IDs are present
3105+ self .assertIn (parent_ingredient_id , json_data )
3106+ self .assertIn (placed_ingredient_id , json_data )
3107+
3108+ # Verify both actions are present
3109+ self .assertIn ("c2pa.opened" , json_data )
3110+ self .assertIn ("c2pa.placed" , json_data )
3111+
3112+ builder .close ()
3113+
3114+ # Make sure settings are put back to the common test defaults
3115+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3116+
3117+ def test_builder_opened_action_multiple_ingredient_no_auto_add (self ):
3118+ """Test Builder with c2pa.opened and c2pa.placed actions with multiple ingredients"""
3119+ # Disable auto-added actions, as what we are doing here can confuse auto-placements
3120+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3121+
3122+ # Instance IDs for linking ingredients and actions
3123+ # With multiple ingredients, we need multiple different unique ids so they each link properly
3124+ parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
3125+ placed_ingredient_1_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
3126+ placed_ingredient_2_id = "xmp:iid:a965983b-36fb-445a-aa80-f2d712acd14c"
3127+
3128+ manifestDefinition = {
3129+ "claim_generator_info" : [{
3130+ "name" : "Python CAI test" ,
3131+ "version" : "0.2.942"
3132+ }],
3133+ "title" : "A title for the provenance test with multiple ingredients" ,
3134+ "ingredients" : [
3135+ # More ingredients will be added using add_ingredient
3136+ {
3137+ "format" : "jpeg" ,
3138+ "relationship" : "componentOf" ,
3139+ # Instance ID must be generated to match what is in parameters ingredientIds array
3140+ "instance_id" : placed_ingredient_1_id ,
3141+ }
3142+ ],
3143+ "assertions" : [
3144+ {
3145+ "label" : "c2pa.actions.v2" ,
3146+ "data" : {
3147+ "actions" : [
3148+ {
3149+ "action" : "c2pa.opened" ,
3150+ "softwareAgent" : {
3151+ "name" : "A parent opened asset" ,
3152+ },
3153+ "parameters" : {
3154+ "ingredientIds" : [
3155+ parent_ingredient_id
3156+ ]
3157+ },
3158+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3159+ },
3160+ {
3161+ "action" : "c2pa.placed" ,
3162+ "softwareAgent" : {
3163+ "name" : "Component placed assets" ,
3164+ },
3165+ "parameters" : {
3166+ "ingredientIds" : [
3167+ placed_ingredient_1_id ,
3168+ placed_ingredient_2_id
3169+ ]
3170+ },
3171+ "digitalSourceType" : "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
3172+ }
3173+ ]
3174+ }
3175+ }
3176+ ]
3177+ }
3178+
3179+ # The ingredient json for the opened action needs to match the instance_id in the manifestDefinition,
3180+ # so that ingredients properly link with their action
3181+ ingredient_json_parent = {
3182+ "relationship" : "parentOf" ,
3183+ "instance_id" : parent_ingredient_id
3184+ }
3185+
3186+ # The ingredient json for the placed action needs to match the instance_id in the manifestDefinition,
3187+ # so that ingredients properly link with their action
3188+ ingredient_json_placed = {
3189+ "relationship" : "componentOf" ,
3190+ "instance_id" : placed_ingredient_2_id
3191+ }
3192+
3193+ # Read the input file (A.jpg will be signed)
3194+ with open (self .testPath2 , "rb" ) as test_file :
3195+ file_content = test_file .read ()
3196+
3197+ builder = Builder .from_json (manifestDefinition )
3198+
3199+ # Add C.jpg as the parent ingredient (for c2pa.opened, it's the opened asset)
3200+ with open (self .testPath , 'rb' ) as f1 :
3201+ builder .add_ingredient (ingredient_json_parent , "image/jpeg" , f1 )
3202+
3203+ # Add cloud.jpg as another placed ingredient (for instance, added on the opened asset)
3204+ with open (self .testPath4 , 'rb' ) as f2 :
3205+ builder .add_ingredient (ingredient_json_placed , "image/jpeg" , f2 )
3206+
3207+ output_buffer = io .BytesIO (bytearray ())
3208+ builder .sign (
3209+ self .signer ,
3210+ "image/jpeg" ,
3211+ io .BytesIO (file_content ),
3212+ output_buffer )
3213+ output_buffer .seek (0 )
3214+
3215+ # Read and verify the manifest
3216+ reader = Reader ("image/jpeg" , output_buffer )
3217+ json_data = reader .json ()
3218+ manifest_data = json .loads (json_data )
3219+
3220+ # Verify all ingredient instance IDs are present
3221+ self .assertIn (parent_ingredient_id , json_data )
3222+ self .assertIn (placed_ingredient_1_id , json_data )
3223+ self .assertIn (placed_ingredient_2_id , json_data )
3224+
3225+ # Verify both actions are present
3226+ self .assertIn ("c2pa.opened" , json_data )
3227+ self .assertIn ("c2pa.placed" , json_data )
3228+
3229+ builder .close ()
3230+
3231+ # Make sure settings are put back to the common test defaults
3232+ load_settings ('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}' )
3233+
29273234
29283235class TestStream (unittest .TestCase ):
29293236 def setUp (self ):
0 commit comments