Skip to content

Commit 476eb23

Browse files
authored
chore: Add c2pa.opened examples (#202)
* fix: Initial example * fix: Switch to other runner
1 parent ac1018e commit 476eb23

File tree

3 files changed

+310
-3
lines changed

3 files changed

+310
-3
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ jobs:
368368
- target: arm64
369369
runs-on: macos-latest
370370
- target: x86_64
371-
runs-on: macos-13
371+
runs-on: macos-15-intel
372372

373373
if: |
374374
github.event_name != 'pull_request' ||
@@ -387,7 +387,7 @@ jobs:
387387
- target: arm64
388388
runs-on: macos-latest
389389
- target: x86_64
390-
runs-on: macos-13
390+
runs-on: macos-15-intel
391391

392392
if: |
393393
github.event_name != 'pull_request' ||

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ run-examples:
3737

3838
# Runs the examples, then the unit tests
3939
test:
40-
make run-examples
40+
make run-examples
4141
python3 ./tests/test_unit_tests.py
4242
python3 ./tests/test_unit_tests_threaded.py
4343

tests/test_unit_tests.py

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

29283235
class TestStream(unittest.TestCase):
29293236
def setUp(self):

0 commit comments

Comments
 (0)