From 6aab2e653d6a4fb8e9dea7aba715f67acc50789e Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 9 Oct 2025 10:30:48 -0700 Subject: [PATCH 01/13] fix: Initial test --- pyproject.toml | 2 +- src/c2pa/c2pa.py | 37 ++++++++++++++++++++ tests/test_unit_tests.py | 73 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a98b4a7..f872e362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "c2pa-python" -version = "0.26.0" +version = "0.27.0" requires-python = ">=3.10" description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library" readme = { file = "README.md", content-type = "text/markdown" } diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 99617750..442a52bd 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -49,6 +49,7 @@ 'c2pa_builder_set_remote_url', 'c2pa_builder_add_resource', 'c2pa_builder_add_ingredient_from_stream', + 'c2pa_builder_add_action', 'c2pa_builder_to_archive', 'c2pa_builder_sign', 'c2pa_manifest_bytes_free', @@ -403,6 +404,9 @@ def _setup_function(func, argtypes, restype=None): ctypes.c_char_p, ctypes.POINTER(C2paStream)], ctypes.c_int) +_setup_function(_lib.c2pa_builder_add_action, + [ctypes.POINTER(C2paBuilder), ctypes.c_char_p], + ctypes.c_int) _setup_function(_lib.c2pa_builder_to_archive, [ctypes.POINTER(C2paBuilder), ctypes.POINTER(C2paStream)], ctypes.c_int) @@ -2198,6 +2202,7 @@ class Builder: 'url_error': "Error setting remote URL: {}", 'resource_error': "Error adding resource: {}", 'ingredient_error': "Error adding ingredient: {}", + 'action_error': "Error adding action: {}", 'archive_error': "Error writing archive: {}", 'sign_error': "Error during signing: {}", 'encoding_error': "Invalid UTF-8 characters in manifest: {}", @@ -2613,6 +2618,38 @@ def add_ingredient_from_file_path( except Exception as e: raise C2paError.Other(f"Could not add ingredient: {e}") from e + def add_action(self, action_json: str) -> None: + """Add an action to the builder, that will be placed + in the actions assertion array in the generated manifest. + + Args: + action_json: The JSON action definition + + Raises: + C2paError: If there was an error adding the action + C2paError.Encoding: If the action JSON contains invalid UTF-8 characters + """ + self._ensure_valid_state() + + try: + action_str = action_json.encode('utf-8') + except UnicodeError as e: + raise C2paError.Encoding( + Builder._ERROR_MESSAGES['encoding_error'].format(str(e)) + ) + + result = _lib.c2pa_builder_add_action(self._builder, action_str) + + if result != 0: + error = _parse_operation_result_for_error(_lib.c2pa_error()) + if error: + raise C2paError(error) + raise C2paError( + Builder._ERROR_MESSAGES['action_error'].format( + "Unknown error" + ) + ) + def to_archive(self, stream: Any) -> None: """Write an archive of the builder to a stream. diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 7575aba7..77ca92dd 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -733,6 +733,7 @@ def setUp(self): "actions": [ { "action": "c2pa.opened" + # Should have more parameters here, but omitted in tests } ] } @@ -2323,6 +2324,78 @@ def test_builder_state_with_invalid_native_pointer(self): with self.assertRaises(Error): builder.set_no_embed() + def test_builder_minimal_manifest_add_actions_and_sign(self): + # For testing, remove auto-added actions + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled": false}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled": false}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled": false}}}}') + + initial_manifest_definition = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + "format": "image/jpeg", + "title": "Python Test Image V2", + } + builder = Builder.from_json(self.manifestDefinition) + + builder.add_action('{ "action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}') + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + print(json_data) + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + + # Check what we added is there + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.created" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.created": + created_action_found = True + break + + self.assertTrue(created_action_found) + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled": true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled": true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled": true}}}}') + class TestStream(unittest.TestCase): def setUp(self): From 6fa6ca853940fb9818956e91ac528b6669fbcbab Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 9 Oct 2025 11:12:39 -0700 Subject: [PATCH 02/13] fix: add the tests with settings --- tests/test_unit_tests.py | 110 ++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 77ca92dd..9e3a0427 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -749,7 +749,7 @@ def setUp(self): "version": "0.0.1", }], # claim version 2 is the default - # "claim_version": 2, + "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", "ingredients": [], @@ -2324,11 +2324,11 @@ def test_builder_state_with_invalid_native_pointer(self): with self.assertRaises(Error): builder.set_no_embed() - def test_builder_minimal_manifest_add_actions_and_sign(self): + def test_builder_add_action_to_manifest_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled": false}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled": false}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled": false}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator": "python_test", @@ -2336,12 +2336,29 @@ def test_builder_minimal_manifest_add_actions_and_sign(self): "name": "python_test", "version": "0.0.1", }], + # claim version 2 is the default + "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", + "ingredients": [], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation" + } + ] + } + } + ] } - builder = Builder.from_json(self.manifestDefinition) + builder = Builder.from_json(initial_manifest_definition) - builder.add_action('{ "action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}') + action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' + builder.add_action(action_json) with open(self.testPath2, "rb") as file: output = io.BytesIO(bytearray()) @@ -2349,7 +2366,6 @@ def test_builder_minimal_manifest_add_actions_and_sign(self): output.seek(0) reader = Reader("image/jpeg", output) json_data = reader.json() - print(json_data) manifest_data = json.loads(json_data) # Verify active manifest exists @@ -2365,7 +2381,7 @@ def test_builder_minimal_manifest_add_actions_and_sign(self): self.assertIn("assertions", active_manifest) assertions = active_manifest["assertions"] - # Find the c2pa.actions.v2 assertion + # Find the c2pa.actions.v2 assertion to check what we added actions_assertion = None for assertion in assertions: if assertion.get("label") == "c2pa.actions.v2": @@ -2373,8 +2389,76 @@ def test_builder_minimal_manifest_add_actions_and_sign(self): break self.assertIsNotNone(actions_assertion) + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.color_adjustments" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.color_adjustments": + created_action_found = True + break - # Check what we added is there + self.assertTrue(created_action_found) + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + + + def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): + # For testing, remove auto-added actions + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":false}}}}') + + initial_manifest_definition = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + "format": "image/jpeg", + "title": "Python Test Image V2", + } + + builder = Builder.from_json(initial_manifest_definition) + builder.add_action('{ "action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}') + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion to look for what we added + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) self.assertIn("data", actions_assertion) assertion_data = actions_assertion["data"] # Verify the manifest now contains actions @@ -2392,9 +2476,9 @@ def test_builder_minimal_manifest_add_actions_and_sign(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled": true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled": true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled": true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') class TestStream(unittest.TestCase): From f080d40d526f69c5dd0eba47144f6581d96f99ff Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 9 Oct 2025 11:21:42 -0700 Subject: [PATCH 03/13] fix: Settings test --- tests/test_unit_tests.py | 94 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 9e3a0427..f5ad40c6 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -2410,6 +2410,100 @@ def test_builder_add_action_to_manifest_no_auto_add(self): load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + def test_builder_add_action_to_manifest_with_auto_add(self): + # For testing, force settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + + initial_manifest_definition = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + # claim version 2 is the default + "claim_version": 2, + "format": "image/jpeg", + "title": "Python Test Image V2", + "ingredients": [], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation" + } + ] + } + } + ] + } + builder = Builder.from_json(initial_manifest_definition) + + action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' + builder.add_action(action_json) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion to check what we added + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.color_adjustments" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.color_adjustments": + created_action_found = True + break + + self.assertTrue(created_action_found) + + # Verify "c2pa.created" action exists only once in the actions array + created_count = 0 + for action in actions: + if action.get("action") == "c2pa.created": + created_count += 1 + + self.assertEqual(created_count, 1, "c2pa.created action should appear exactly once") + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): # For testing, remove auto-added actions From 4474a53101c4258d246a834de264f6d6742abf18 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 9 Oct 2025 13:29:14 -0700 Subject: [PATCH 04/13] fix: Add_action API --- tests/test_unit_tests.py | 82 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index f5ad40c6..1f9cf98c 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -2499,12 +2499,11 @@ def test_builder_add_action_to_manifest_with_auto_add(self): builder.close() - # Reset settings + # Reset settings to default load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') - def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): # For testing, remove auto-added actions load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false}}}}') @@ -2574,6 +2573,85 @@ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): + # For testing, remove auto-added actions + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + + initial_manifest_definition = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + "format": "image/jpeg", + "title": "Python Test Image V2", + } + + builder = Builder.from_json(initial_manifest_definition) + action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}' + builder.add_action(action_json) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion to look for what we added + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.created" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.created": + created_action_found = True + break + + self.assertTrue(created_action_found) + + # Verify "c2pa.color_adjustments" action also exists in the same actions array + color_adjustments_found = False + for action in actions: + if action.get("action") == "c2pa.color_adjustments": + color_adjustments_found = True + break + + self.assertTrue(color_adjustments_found) + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + class TestStream(unittest.TestCase): def setUp(self): From 36530ab59e78145162c675085e7a2a9f3d12e3b7 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 9 Oct 2025 13:31:45 -0700 Subject: [PATCH 05/13] fix: COmments --- tests/test_unit_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 1f9cf98c..4511a7ac 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -749,7 +749,7 @@ def setUp(self): "version": "0.0.1", }], # claim version 2 is the default - "claim_version": 2, + # "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", "ingredients": [], @@ -2337,7 +2337,7 @@ def test_builder_add_action_to_manifest_no_auto_add(self): "version": "0.0.1", }], # claim version 2 is the default - "claim_version": 2, + # "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", "ingredients": [], @@ -2423,7 +2423,7 @@ def test_builder_add_action_to_manifest_with_auto_add(self): "version": "0.0.1", }], # claim version 2 is the default - "claim_version": 2, + # "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", "ingredients": [], From f15d98ead02ca83fdd33d0bf0a61597a1b3d6342 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:09:08 -0700 Subject: [PATCH 06/13] fix: Review comments --- tests/test_unit_tests.py | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 4511a7ac..6c23b340 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -743,7 +743,6 @@ def setUp(self): # Define a V2 manifest as a dictionary self.manifestDefinitionV2 = { - "claim_generator": "python_test", "claim_generator_info": [{ "name": "python_test", "version": "0.0.1", @@ -2326,12 +2325,9 @@ def test_builder_state_with_invalid_native_pointer(self): def test_builder_add_action_to_manifest_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":false}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { - "claim_generator": "python_test", "claim_generator_info": [{ "name": "python_test", "version": "0.0.1", @@ -2340,7 +2336,6 @@ def test_builder_add_action_to_manifest_no_auto_add(self): # "claim_version": 2, "format": "image/jpeg", "title": "Python Test Image V2", - "ingredients": [], "assertions": [ { "label": "c2pa.actions", @@ -2406,18 +2401,13 @@ def test_builder_add_action_to_manifest_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') def test_builder_add_action_to_manifest_with_auto_add(self): # For testing, force settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') initial_manifest_definition = { - "claim_generator": "python_test", "claim_generator_info": [{ "name": "python_test", "version": "0.0.1", @@ -2500,15 +2490,11 @@ def test_builder_add_action_to_manifest_with_auto_add(self): builder.close() # Reset settings to default - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":false}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":false}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') initial_manifest_definition = { "claim_generator": "python_test", @@ -2569,18 +2555,13 @@ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): # For testing, remove auto-added actions - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') initial_manifest_definition = { - "claim_generator": "python_test", "claim_generator_info": [{ "name": "python_test", "version": "0.0.1", @@ -2648,9 +2629,7 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): builder.close() # Reset settings - load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_opened_action":{"enabled":true}}}}') - load_settings('{"builder":{"actions":{"auto_created_action":{"enabled":true}}}}') + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') class TestStream(unittest.TestCase): From 5c01765971a0dc5c2d3c6e2da38ffd2fc36c42ba Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:13:45 -0700 Subject: [PATCH 07/13] fix: From dict API --- src/c2pa/c2pa.py | 8 +++- tests/test_unit_tests.py | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 442a52bd..30e51ab3 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -2618,12 +2618,12 @@ def add_ingredient_from_file_path( except Exception as e: raise C2paError.Other(f"Could not add ingredient: {e}") from e - def add_action(self, action_json: str) -> None: + def add_action(self, action_json: Union[str, dict]) -> None: """Add an action to the builder, that will be placed in the actions assertion array in the generated manifest. Args: - action_json: The JSON action definition + action_json: The JSON action definition (either a JSON string or a dictionary) Raises: C2paError: If there was an error adding the action @@ -2631,6 +2631,10 @@ def add_action(self, action_json: str) -> None: """ self._ensure_valid_state() + # Convert dict to JSON string if necessary + if isinstance(action_json, dict): + action_json = json.dumps(action_json) + try: action_str = action_json.encode('utf-8') except UnicodeError as e: diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 6c23b340..e7e1cf54 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -2403,6 +2403,87 @@ def test_builder_add_action_to_manifest_no_auto_add(self): # Reset settings load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + def test_builder_add_action_to_manifest_from_dict_no_auto_add(self): + # For testing, remove auto-added actions + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + + initial_manifest_definition = { + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + # claim version 2 is the default + # "claim_version": 2, + "format": "image/jpeg", + "title": "Python Test Image V2", + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation" + } + ] + } + } + ] + } + builder = Builder.from_json(initial_manifest_definition) + + # Using a dictionary instead of a JSON string + action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}} + builder.add_action(action_dict) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion to check what we added + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.color_adjustments" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.color_adjustments": + created_action_found = True + break + + self.assertTrue(created_action_found) + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + def test_builder_add_action_to_manifest_with_auto_add(self): # For testing, force settings load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') From e6f7de52dcd08bcde8acc3b57ce73ee302583d33 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:26:30 -0700 Subject: [PATCH 08/13] fix: dict APIs --- src/c2pa/c2pa.py | 16 ++-- tests/test_unit_tests.py | 191 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 6 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 30e51ab3..5630a8f2 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -2500,13 +2500,13 @@ def add_resource(self, uri: str, stream: Any): ) ) - def add_ingredient(self, ingredient_json: str, format: str, source: Any): + def add_ingredient(self, ingredient_json: Union[str, dict], format: str, source: Any): """Add an ingredient to the builder (facade method). The added ingredient's source should be a stream-like object (for instance, a file opened as stream). Args: - ingredient_json: The JSON ingredient definition + ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient source: The stream containing the ingredient data (any Python stream-like object) @@ -2526,14 +2526,14 @@ def add_ingredient(self, ingredient_json: str, format: str, source: Any): def add_ingredient_from_stream( self, - ingredient_json: str, + ingredient_json: Union[str, dict], format: str, source: Any): """Add an ingredient from a stream to the builder. Explicitly named API requiring a stream as input parameter. Args: - ingredient_json: The JSON ingredient definition + ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient source: The stream containing the ingredient data (any Python stream-like object) @@ -2545,6 +2545,10 @@ def add_ingredient_from_stream( """ self._ensure_valid_state() + # Convert dict to JSON string if necessary + if isinstance(ingredient_json, dict): + ingredient_json = json.dumps(ingredient_json) + try: ingredient_str = ingredient_json.encode('utf-8') format_str = format.encode('utf-8') @@ -2575,7 +2579,7 @@ def add_ingredient_from_stream( def add_ingredient_from_file_path( self, - ingredient_json: str, + ingredient_json: Union[str, dict], format: str, filepath: Union[str, Path]): """Add an ingredient from a file path to the builder (deprecated). @@ -2586,7 +2590,7 @@ def add_ingredient_from_file_path( Use :meth:`add_ingredient` with a file stream instead. Args: - ingredient_json: The JSON ingredient definition + ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient filepath: The path to the file containing the ingredient data (can be a string or Path object) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index e7e1cf54..79b729e4 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -1289,6 +1289,17 @@ def test_builder_add_ingredient(self): builder.close() + def test_builder_add_ingredient_dict(self): + builder = Builder.from_json(self.manifestDefinition) + assert builder._builder is not None + + # Test adding ingredient with a dictionary instead of JSON string + ingredient_dict = {"test": "ingredient"} + with open(self.testPath, 'rb') as f: + builder.add_ingredient(ingredient_dict, "image/jpeg", f) + + builder.close() + def test_builder_add_multiple_ingredients(self): builder = Builder.from_json(self.manifestDefinition) assert builder._builder is not None @@ -1309,6 +1320,26 @@ def test_builder_add_multiple_ingredients(self): builder.close() + def test_builder_add_multiple_ingredients_2(self): + builder = Builder.from_json(self.manifestDefinition) + assert builder._builder is not None + + # Test builder operations + builder.set_no_embed() + builder.set_remote_url("http://test.url") + + # Test adding ingredient with a dictionary + ingredient_dict = {"test": "ingredient"} + with open(self.testPath, 'rb') as f: + builder.add_ingredient(ingredient_dict, "image/jpeg", f) + + # Test adding another ingredient with a JSON string + ingredient_json = '{"test": "ingredient2"}' + with open(self.testPath2, 'rb') as f: + builder.add_ingredient(ingredient_json, "image/png", f) + + builder.close() + def test_builder_add_multiple_ingredients_and_resources(self): builder = Builder.from_json(self.manifestDefinition) assert builder._builder is not None @@ -1537,6 +1568,46 @@ def test_builder_sign_with_ingredient_from_stream(self): builder.close() + def test_builder_sign_with_ingredient_dict_from_stream(self): + builder = Builder.from_json(self.manifestDefinition) + assert builder._builder is not None + + # Test adding ingredient using stream with a dictionary + ingredient_dict = {"title": "Test Ingredient Stream"} + with open(self.testPath3, 'rb') as f: + builder.add_ingredient_from_stream( + ingredient_dict, "image/jpeg", f) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify ingredients array exists in active manifest + self.assertIn("ingredients", active_manifest) + self.assertIsInstance(active_manifest["ingredients"], list) + self.assertTrue(len(active_manifest["ingredients"]) > 0) + + # Verify the first ingredient's title matches what we set + first_ingredient = active_manifest["ingredients"][0] + self.assertEqual( + first_ingredient["title"], + "Test Ingredient Stream") + + builder.close() + def test_builder_sign_with_multiple_ingredient(self): builder = Builder.from_json(self.manifestDefinition) assert builder._builder is not None @@ -2712,6 +2783,86 @@ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self): # Reset settings load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + def test_builder_sign_dicts_no_auto_add(self): + # For testing, remove auto-added actions + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}') + + initial_manifest_definition = { + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + # claim version 2 is the default + # "claim_version": 2, + "format": "image/jpeg", + "title": "Python Test Image V2", + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.created", + "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation" + } + ] + } + } + ] + } + builder = Builder.from_json(initial_manifest_definition) + + # Using a dictionary instead of a JSON string + action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}} + builder.add_action(action_dict) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify assertions object exists in active manifest + self.assertIn("assertions", active_manifest) + assertions = active_manifest["assertions"] + + # Find the c2pa.actions.v2 assertion to check what we added + actions_assertion = None + for assertion in assertions: + if assertion.get("label") == "c2pa.actions.v2": + actions_assertion = assertion + break + + self.assertIsNotNone(actions_assertion) + self.assertIn("data", actions_assertion) + assertion_data = actions_assertion["data"] + # Verify the manifest now contains actions + self.assertIn("actions", assertion_data) + actions = assertion_data["actions"] + # Verify "c2pa.color_adjustments" action exists anywhere in the actions array + created_action_found = False + for action in actions: + if action.get("action") == "c2pa.color_adjustments": + created_action_found = True + break + + self.assertTrue(created_action_found) + + builder.close() + + # Reset settings + load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') class TestStream(unittest.TestCase): def setUp(self): @@ -3175,6 +3326,46 @@ def test_builder_sign_with_ingredient_from_file(self): builder.close() + def test_builder_sign_with_ingredient_dict_from_file(self): + """Test Builder class operations with an ingredient added from file path using a dictionary.""" + + builder = Builder.from_json(self.manifestDefinition) + + # Test adding ingredient from file path with a dictionary + ingredient_dict = {"title": "Test Ingredient From File"} + # Suppress the specific deprecation warning for this test, as this is a legacy method + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # Verify ingredients array exists in active manifest + self.assertIn("ingredients", active_manifest) + self.assertIsInstance(active_manifest["ingredients"], list) + self.assertTrue(len(active_manifest["ingredients"]) > 0) + + # Verify the first ingredient's title matches what we set + first_ingredient = active_manifest["ingredients"][0] + self.assertEqual(first_ingredient["title"], "Test Ingredient From File") + + builder.close() + def test_builder_add_ingredient_from_file_path(self): """Test Builder class add_ingredient_from_file_path method.""" From dc54dceeeaaced31523b0e3507e6978998b945a5 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:39:08 -0700 Subject: [PATCH 09/13] fix: dict support --- src/c2pa/c2pa.py | 39 ++++++++++++++++++++++++++------ tests/test_unit_tests.py | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 5630a8f2..8aea2321 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -671,20 +671,45 @@ def version() -> str: return _convert_to_py_string(result) +@overload def load_settings(settings: str, format: str = "json") -> None: - """Load C2PA settings from a string. + ... + + +@overload +def load_settings(settings: dict) -> None: + ... + + +def load_settings(settings: Union[str, dict], format: str = "json") -> None: + """Load C2PA settings from a string or dict. Args: - settings: The settings string to load - format: The format of the settings string (default: "json") + settings: The settings string or dict to load + format: The format of the settings string (default: "json"). + Ignored when settings is a dict. Raises: C2paError: If there was an error loading the settings """ - result = _lib.c2pa_load_settings( - settings.encode('utf-8'), - format.encode('utf-8') - ) + # If settings is a dict, convert it to JSON string and set format + try: + if isinstance(settings, dict): + settings_str = json.dumps(settings) + format = "json" + else: + settings_str = settings + except (TypeError, ValueError) as e: + raise C2paError(f"Failed to serialize settings to JSON: {e}") + + # Encode strings to UTF-8 bytes outside of the C call + try: + settings_bytes = settings_str.encode('utf-8') + format_bytes = format.encode('utf-8') + except (AttributeError, UnicodeEncodeError) as e: + raise C2paError(f"Failed to encode settings to UTF-8: {e}") + + result = _lib.c2pa_load_settings(settings_bytes, format_bytes) if result != 0: error = _parse_operation_result_for_error(_lib.c2pa_error()) if error: diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 79b729e4..7bdb8897 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -1482,6 +1482,53 @@ def test_builder_sign_with_setting_no_thumbnail_and_ingredient(self): # Settings are thread-local, so we reset to the default "true" here load_settings('{"builder": { "thumbnail": {"enabled": true}}}') + def test_builder_sign_with_settingdict_no_thumbnail_and_ingredient(self): + builder = Builder.from_json(self.manifestDefinition) + assert builder._builder is not None + + # The following removes the manifest's thumbnail - using dict instead of string + load_settings({"builder": {"thumbnail": {"enabled": False}}}) + + # Test adding ingredient + ingredient_json = '{ "title": "Test Ingredient" }' + with open(self.testPath3, 'rb') as f: + builder.add_ingredient(ingredient_json, "image/jpeg", f) + + with open(self.testPath2, "rb") as file: + output = io.BytesIO(bytearray()) + builder.sign(self.signer, "image/jpeg", file, output) + output.seek(0) + reader = Reader("image/jpeg", output) + json_data = reader.json() + manifest_data = json.loads(json_data) + + # Verify active manifest exists + self.assertIn("active_manifest", manifest_data) + active_manifest_id = manifest_data["active_manifest"] + + # Verify active manifest object exists + self.assertIn("manifests", manifest_data) + self.assertIn(active_manifest_id, manifest_data["manifests"]) + active_manifest = manifest_data["manifests"][active_manifest_id] + + # There should be no thumbnail anymore here + self.assertNotIn("thumbnail", active_manifest) + + # Verify ingredients array exists in active manifest + self.assertIn("ingredients", active_manifest) + self.assertIsInstance(active_manifest["ingredients"], list) + self.assertTrue(len(active_manifest["ingredients"]) > 0) + + # Verify the first ingredient's title matches what we set + first_ingredient = active_manifest["ingredients"][0] + self.assertEqual(first_ingredient["title"], "Test Ingredient") + self.assertNotIn("thumbnail", first_ingredient) + + builder.close() + + # Settings are thread-local, so we reset to the default "true" here - using dict instead of string + load_settings({"builder": {"thumbnail": {"enabled": True}}}) + def test_builder_sign_with_duplicate_ingredient(self): builder = Builder.from_json(self.manifestDefinition) assert builder._builder is not None @@ -2864,6 +2911,7 @@ def test_builder_sign_dicts_no_auto_add(self): # Reset settings load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}') + class TestStream(unittest.TestCase): def setUp(self): self.temp_file = io.BytesIO() From de7fb3599c4361091c28adba1c55caa17bb36c04 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:42:23 -0700 Subject: [PATCH 10/13] fix: Fromat 1 --- src/c2pa/c2pa.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 8aea2321..6d5ece59 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -686,7 +686,7 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: Args: settings: The settings string or dict to load - format: The format of the settings string (default: "json"). + format: The format of the settings string (default: "json"). Ignored when settings is a dict. Raises: @@ -2531,7 +2531,8 @@ def add_ingredient(self, ingredient_json: Union[str, dict], format: str, source: (for instance, a file opened as stream). Args: - ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) + ingredient_json: The JSON ingredient definition + (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient source: The stream containing the ingredient data (any Python stream-like object) @@ -2558,7 +2559,8 @@ def add_ingredient_from_stream( Explicitly named API requiring a stream as input parameter. Args: - ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) + ingredient_json: The JSON ingredient definition + (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient source: The stream containing the ingredient data (any Python stream-like object) @@ -2615,7 +2617,8 @@ def add_ingredient_from_file_path( Use :meth:`add_ingredient` with a file stream instead. Args: - ingredient_json: The JSON ingredient definition (either a JSON string or a dictionary) + ingredient_json: The JSON ingredient definition + (either a JSON string or a dictionary) format: The MIME type or extension of the ingredient filepath: The path to the file containing the ingredient data (can be a string or Path object) @@ -2652,11 +2655,12 @@ def add_action(self, action_json: Union[str, dict]) -> None: in the actions assertion array in the generated manifest. Args: - action_json: The JSON action definition (either a JSON string or a dictionary) + action_json: The JSON action definition + (either a JSON string or a dictionary) Raises: C2paError: If there was an error adding the action - C2paError.Encoding: If the action JSON contains invalid UTF-8 characters + C2paError.Encoding: If the action JSON contains invalid UTF-8 chars """ self._ensure_valid_state() From 2fa587bf464b40e5fe8752fb5d9c12d5af1a7979 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:43:40 -0700 Subject: [PATCH 11/13] fix: Format 2 --- src/c2pa/c2pa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 6d5ece59..f14c2580 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -2525,7 +2525,9 @@ def add_resource(self, uri: str, stream: Any): ) ) - def add_ingredient(self, ingredient_json: Union[str, dict], format: str, source: Any): + def add_ingredient( + self, ingredient_json: Union[str, dict], format: str, source: Any + ): """Add an ingredient to the builder (facade method). The added ingredient's source should be a stream-like object (for instance, a file opened as stream). From 9b782327e7a8cf594d320d7daffaf893d4fd2e6c Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 20:59:24 -0700 Subject: [PATCH 12/13] fix: COmments --- src/c2pa/c2pa.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index f14c2580..861a9141 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -702,7 +702,6 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: except (TypeError, ValueError) as e: raise C2paError(f"Failed to serialize settings to JSON: {e}") - # Encode strings to UTF-8 bytes outside of the C call try: settings_bytes = settings_str.encode('utf-8') format_bytes = format.encode('utf-8') @@ -2574,7 +2573,7 @@ def add_ingredient_from_stream( """ self._ensure_valid_state() - # Convert dict to JSON string if necessary + # Convert dict to JSON string as necessary if isinstance(ingredient_json, dict): ingredient_json = json.dumps(ingredient_json) @@ -2666,7 +2665,7 @@ def add_action(self, action_json: Union[str, dict]) -> None: """ self._ensure_valid_state() - # Convert dict to JSON string if necessary + # Convert dict to JSON string as necessary if isinstance(action_json, dict): action_json = json.dumps(action_json) From c9b2892730cf34ce18b2da42a5dbdbadbf70a2be Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Fri, 10 Oct 2025 21:03:48 -0700 Subject: [PATCH 13/13] fix: Format --- src/c2pa/c2pa.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 861a9141..b0b7e5db 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -692,7 +692,7 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None: Raises: C2paError: If there was an error loading the settings """ - # If settings is a dict, convert it to JSON string and set format + # Convert to JSON string as necessary try: if isinstance(settings, dict): settings_str = json.dumps(settings) @@ -2573,7 +2573,6 @@ def add_ingredient_from_stream( """ self._ensure_valid_state() - # Convert dict to JSON string as necessary if isinstance(ingredient_json, dict): ingredient_json = json.dumps(ingredient_json) @@ -2665,7 +2664,6 @@ def add_action(self, action_json: Union[str, dict]) -> None: """ self._ensure_valid_state() - # Convert dict to JSON string as necessary if isinstance(action_json, dict): action_json = json.dumps(action_json)