From a947119e579fc3edbcef7c35c7f94a5caf0dbd86 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Thu, 5 Mar 2026 15:10:51 -0800 Subject: [PATCH 1/6] fix: New API --- src/c2pa/c2pa.py | 92 ++++++++++++++++++++++++++++------------ tests/test_unit_tests.py | 36 ++++++++++++++++ 2 files changed, 102 insertions(+), 26 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index d3988ade..96ebb4fe 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -78,6 +78,7 @@ 'c2pa_context_new', 'c2pa_reader_from_context', 'c2pa_reader_with_stream', + 'c2pa_reader_with_context_from_manifest_data_and_stream', 'c2pa_builder_from_context', 'c2pa_builder_with_definition', 'c2pa_free', @@ -696,6 +697,13 @@ def _setup_function(func, argtypes, restype=None): ctypes.POINTER(C2paStream)], ctypes.POINTER(C2paReader) ) +_setup_function( + _lib.c2pa_reader_with_context_from_manifest_data_and_stream, + [ctypes.POINTER(C2paContext), ctypes.c_char_p, + ctypes.POINTER(C2paStream), + ctypes.POINTER(ctypes.c_ubyte), ctypes.c_size_t], + ctypes.POINTER(C2paReader), +) _setup_function( _lib.c2pa_builder_from_context, [ctypes.POINTER(C2paContext)], @@ -2233,6 +2241,7 @@ def __init__( if context is not None: self._init_from_context( context, format_or_path, stream, + manifest_data, ) return @@ -2346,10 +2355,12 @@ def _init_from_file(self, path, format_bytes, str(e))) def _init_from_context(self, context, format_or_path, - stream): + stream, manifest_data=None): """Initialize Reader from a ContextProvider. - Uses c2pa_reader_from_context + c2pa_reader_with_stream. + Uses c2pa_reader_from_context + c2pa_reader_with_stream, + or c2pa_reader_with_context_from_manifest_data_and_stream + when manifest_data is provided. """ if not context.is_valid: raise C2paError("Context is not valid") @@ -2378,33 +2389,62 @@ def _init_from_context(self, context, format_or_path, self._own_stream = Stream(stream) try: - # Create base reader from context - reader_ptr = _lib.c2pa_reader_from_context( - context.execution_context, - ) - if not reader_ptr: - _parse_operation_result_for_error(_lib.c2pa_error()) - raise C2paError( - Reader._ERROR_MESSAGES[ - 'reader_error' - ].format("Unknown error") + if manifest_data is not None: + if not isinstance(manifest_data, bytes): + raise TypeError( + Reader._ERROR_MESSAGES[ + 'manifest_error']) + manifest_array = ( + ctypes.c_ubyte * + len(manifest_data))( + *manifest_data) + new_ptr = ( + _lib.c2pa_reader_with_context_from_manifest_data_and_stream( + context.execution_context, + format_bytes, + self._own_stream._stream, + manifest_array, + len(manifest_data), + ) ) + if not new_ptr: + _parse_operation_result_for_error( + _lib.c2pa_error()) + raise C2paError( + Reader._ERROR_MESSAGES[ + 'reader_error' + ].format("Unknown error") + ) + else: + # Two-step: from_context + with_stream + reader_ptr = _lib.c2pa_reader_from_context( + context.execution_context, + ) + if not reader_ptr: + _parse_operation_result_for_error( + _lib.c2pa_error()) + raise C2paError( + Reader._ERROR_MESSAGES[ + 'reader_error' + ].format("Unknown error") + ) - # Consume-and-return: reader_ptr is consumed, - # new_ptr is the valid pointer going forward - new_ptr = _lib.c2pa_reader_with_stream( - reader_ptr, format_bytes, - self._own_stream._stream, - ) - # reader_ptr has been invalidated(consumed) - - if not new_ptr: - _parse_operation_result_for_error(_lib.c2pa_error()) - raise C2paError( - Reader._ERROR_MESSAGES[ - 'reader_error' - ].format("Unknown error") + # Consume-and-return: reader_ptr is + # consumed, new_ptr is valid going forward + new_ptr = _lib.c2pa_reader_with_stream( + reader_ptr, format_bytes, + self._own_stream._stream, ) + # reader_ptr has been invalidated(consumed) + + if not new_ptr: + _parse_operation_result_for_error( + _lib.c2pa_error()) + raise C2paError( + Reader._ERROR_MESSAGES[ + 'reader_error' + ].format("Unknown error") + ) self._handle = new_ptr self._state = LifecycleState.ACTIVE diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 883721ab..994bb33f 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -5967,6 +5967,42 @@ def test_archive_sign_with_ingredient_trusted_via_context(self): context.close() settings.close() + def test_remote_sign_trusted_via_context(self): + trust_dict = load_test_settings_json() + settings = Settings.from_dict(trust_dict) + context = Context(settings=settings) + signer = self._ctx_make_signer() + builder = Builder( + self.test_manifest, context=context, + ) + builder.set_no_embed() + with open(DEFAULT_TEST_FILE, "rb") as source: + with io.BytesIO() as output_buffer: + manifest_data = builder.sign( + signer, "image/jpeg", + source, output_buffer, + ) + output_buffer.seek(0) + read_buffer = io.BytesIO( + output_buffer.getvalue() + ) + reader = Reader( + "image/jpeg", read_buffer, + manifest_data, context=context, + ) + validation_state = ( + reader.get_validation_state() + ) + self.assertEqual( + validation_state, "Trusted", + ) + reader.close() + read_buffer.close() + builder.close() + signer.close() + context.close() + settings.close() + def test_sign_callback_signer_in_ctx(self): signer = self._ctx_make_callback_signer() context = Context(signer=signer) From 8ad367317cff6ec750d03e7df9e5d05615d72637 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Mon, 9 Mar 2026 15:04:16 -0700 Subject: [PATCH 2/6] fix: Review feedback on c2pa-rs PR --- src/c2pa/c2pa.py | 69 ++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 08567d4d..d465c0e0 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -76,7 +76,7 @@ 'c2pa_context_new', 'c2pa_reader_from_context', 'c2pa_reader_with_stream', - 'c2pa_reader_with_context_from_manifest_data_and_stream', + 'c2pa_reader_with_manifest_data_and_stream', 'c2pa_builder_from_context', 'c2pa_builder_with_definition', 'c2pa_builder_with_archive', @@ -696,8 +696,8 @@ def _setup_function(func, argtypes, restype=None): ctypes.POINTER(C2paReader) ) _setup_function( - _lib.c2pa_reader_with_context_from_manifest_data_and_stream, - [ctypes.POINTER(C2paContext), ctypes.c_char_p, + _lib.c2pa_reader_with_manifest_data_and_stream, + [ctypes.POINTER(C2paReader), ctypes.c_char_p, ctypes.POINTER(C2paStream), ctypes.POINTER(ctypes.c_ubyte), ctypes.c_size_t], ctypes.POINTER(C2paReader), @@ -2360,7 +2360,7 @@ def _init_from_file(self, path, format_bytes, str(e))) def _init_from_context(self, context, format_or_path, - stream): + stream, manifest_data=None): """Initialize Reader from a context object implementing the ContextProvider interface/abstract base class. """ @@ -2391,6 +2391,19 @@ def _init_from_context(self, context, format_or_path, self._own_stream = Stream(stream) try: + # Step 1: create reader from context + reader_ptr = _lib.c2pa_reader_from_context( + context.execution_context, + ) + if not reader_ptr: + _parse_operation_result_for_error( + _lib.c2pa_error()) + raise C2paError( + Reader._ERROR_MESSAGES[ + 'reader_error' + ].format("Unknown error") + ) + if manifest_data is not None: if not isinstance(manifest_data, bytes): raise TypeError( @@ -2400,53 +2413,35 @@ def _init_from_context(self, context, format_or_path, ctypes.c_ubyte * len(manifest_data))( *manifest_data) + # Step 2: consume current reader, + # with manifest data and stream (C FFI pattern), + # to create a new one (switch out) new_ptr = ( - _lib.c2pa_reader_with_context_from_manifest_data_and_stream( - context.execution_context, + _lib.c2pa_reader_with_manifest_data_and_stream( + reader_ptr, format_bytes, self._own_stream._stream, manifest_array, len(manifest_data), ) ) - if not new_ptr: - _parse_operation_result_for_error( - _lib.c2pa_error()) - raise C2paError( - Reader._ERROR_MESSAGES[ - 'reader_error' - ].format("Unknown error") - ) + # reader_ptr has been invalidated(consumed) else: - # Two-step: from_context + with_stream - reader_ptr = _lib.c2pa_reader_from_context( - context.execution_context, - ) - if not reader_ptr: - _parse_operation_result_for_error( - _lib.c2pa_error()) - raise C2paError( - Reader._ERROR_MESSAGES[ - 'reader_error' - ].format("Unknown error") - ) - - # Consume-and-return: reader_ptr is - # consumed, new_ptr is valid going forward + # Step 2: consume reader with stream new_ptr = _lib.c2pa_reader_with_stream( reader_ptr, format_bytes, self._own_stream._stream, ) # reader_ptr has been invalidated(consumed) - if not new_ptr: - _parse_operation_result_for_error( - _lib.c2pa_error()) - raise C2paError( - Reader._ERROR_MESSAGES[ - 'reader_error' - ].format("Unknown error") - ) + if not new_ptr: + _parse_operation_result_for_error( + _lib.c2pa_error()) + raise C2paError( + Reader._ERROR_MESSAGES[ + 'reader_error' + ].format("Unknown error") + ) self._handle = new_ptr self._state = LifecycleState.ACTIVE From 566cbb65fce628c8d9672e9cfdec567090adaae0 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Mon, 9 Mar 2026 15:12:50 -0700 Subject: [PATCH 3/6] fix: Docs --- src/c2pa/c2pa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index 24e6ebdc..42dd94a4 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -2399,7 +2399,7 @@ def _init_from_context(self, context, format_or_path, self._own_stream = Stream(stream) try: - # Step 1: create reader from context + # Create reader from context reader_ptr = _lib.c2pa_reader_from_context( context.execution_context, ) @@ -2421,7 +2421,7 @@ def _init_from_context(self, context, format_or_path, ctypes.c_ubyte * len(manifest_data))( *manifest_data) - # Step 2: consume current reader, + # Consume current reader, # with manifest data and stream (C FFI pattern), # to create a new one (switch out) new_ptr = ( @@ -2435,7 +2435,7 @@ def _init_from_context(self, context, format_or_path, ) # reader_ptr has been invalidated(consumed) else: - # Step 2: consume reader with stream + # Consume reader with stream new_ptr = _lib.c2pa_reader_with_stream( reader_ptr, format_bytes, self._own_stream._stream, From 1c708e2b68a9c534be188e885e0e8bcab8338528 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:38:29 -0700 Subject: [PATCH 4/6] v0.77.1 of c2pa-rs is released --- tests/test_unit_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 6ae6b8a0..3b4a6646 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -70,7 +70,7 @@ def load_test_settings_json(): class TestC2paSdk(unittest.TestCase): def test_sdk_version(self): # This test verifies the native libraries used match the expected version. - self.assertIn("0.77.0", sdk_version()) + self.assertIn("0.77.1", sdk_version()) class TestReader(unittest.TestCase): From 61361615d6595486965dd5625445d42530593f73 Mon Sep 17 00:00:00 2001 From: tmathern <60901087+tmathern@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:38:39 -0700 Subject: [PATCH 5/6] Update c2pa version to v0.77.1 --- c2pa-native-version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c2pa-native-version.txt b/c2pa-native-version.txt index c418262f..5bbbbe60 100644 --- a/c2pa-native-version.txt +++ b/c2pa-native-version.txt @@ -1 +1 @@ -c2pa-v0.77.0 +c2pa-v0.77.1 From d0f3b4e8822da29c359d99231251bfa23f8e9753 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 10 Mar 2026 19:55:41 -0700 Subject: [PATCH 6/6] fix: Merge conflict mistake --- src/c2pa/c2pa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c2pa/c2pa.py b/src/c2pa/c2pa.py index cdf906b5..0cb76ac9 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -2340,7 +2340,7 @@ def _init_from_file(self, path, format_bytes, Reader._ERROR_MESSAGES['io_error'].format(str(e))) def _init_from_context(self, context, format_or_path, - stream): + stream, manifest_data=None): """Initialize Reader from a Context object implementing the ContextProvider interface/abstract base class. """