diff --git a/pyproject.toml b/pyproject.toml index d2d09be0..83afff78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "c2pa-python" -version = "0.24.1" +version = "0.25.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 1ab70ec0..35dd457e 100644 --- a/src/c2pa/c2pa.py +++ b/src/c2pa/c2pa.py @@ -64,6 +64,8 @@ 'c2pa_free_string_array', 'c2pa_reader_supported_mime_types', 'c2pa_builder_supported_mime_types', + 'c2pa_reader_is_embedded', + 'c2pa_reader_remote_url', ] # TODO Bindings: @@ -369,6 +371,16 @@ def _setup_function(func, argtypes, restype=None): [ctypes.POINTER(ctypes.c_size_t)], ctypes.POINTER(ctypes.c_char_p) ) +_setup_function( + _lib.c2pa_reader_is_embedded, + [ctypes.POINTER(C2paReader)], + ctypes.c_bool +) +_setup_function( + _lib.c2pa_reader_remote_url, + [ctypes.POINTER(C2paReader)], + ctypes.c_void_p +) # Set up Builder function prototypes _setup_function( @@ -1662,6 +1674,45 @@ def resource_to_stream(self, uri: str, stream: Any) -> int: return result + def is_embedded(self) -> bool: + """Check if the reader was created from an embedded manifest. + This method determines whether the C2PA manifest is embedded directly + in the asset file or stored remotely. + Returns: + True if the reader was created from an embedded manifest, + False if it was created from a remote manifest + Raises: + C2paError: If there was an error checking the embedded status + """ + self._ensure_valid_state() + + result = _lib.c2pa_reader_is_embedded(self._reader) + + return bool(result) + + def get_remote_url(self) -> Optional[str]: + """Get the remote URL of the manifest if it was obtained remotely. + This method returns the URL from which the C2PA manifest was fetched + if the reader was created from a remote manifest. If the manifest + is embedded in the asset, this will return None. + Returns: + The remote URL as a string if the manifest was obtained remotely, + None if the manifest is embedded or no remote URL is available + Raises: + C2paError: If there was an error getting the remote URL + """ + self._ensure_valid_state() + + result = _lib.c2pa_reader_remote_url(self._reader) + + if result is None: + # No remote URL set (manifest is embedded) + return None + + # Convert the C string to Python string + url_str = _convert_to_py_string(result) + return url_str + class Signer: """High-level wrapper for C2PA Signer operations.""" diff --git a/tests/test_unit_tests.py b/tests/test_unit_tests.py index 51fc0ea9..6904ca86 100644 --- a/tests/test_unit_tests.py +++ b/tests/test_unit_tests.py @@ -342,6 +342,94 @@ def test_reader_state_with_invalid_native_pointer(self): with self.assertRaises(Error): reader.json() + def test_reader_is_embedded(self): + """Test the is_embedded method returns correct values for embedded and remote manifests.""" + + # Test with a fixture which has an embedded manifest + with open(self.testPath, "rb") as file: + reader = Reader("image/jpeg", file) + self.assertTrue(reader.is_embedded()) + reader.close() + + # Test with cloud.jpg fixture which has a remote manifest (not embedded) + cloud_fixture_path = os.path.join(self.data_dir, "cloud.jpg") + with Reader("image/jpeg", cloud_fixture_path) as reader: + self.assertFalse(reader.is_embedded()) + + def test_sign_and_read_is_not_embedded(self): + """Test the is_embedded method returns correct values for remote manifests.""" + + with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file: + certs = cert_file.read() + with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file: + key = key_file.read() + + # Create signer info and signer + signer_info = C2paSignerInfo( + alg=b"es256", + sign_cert=certs, + private_key=key, + ta_url=b"http://timestamp.digicert.com" + ) + signer = Signer.from_info(signer_info) + + # Define a simple manifest + manifest_definition = { + "claim_generator": "python_test", + "claim_generator_info": [{ + "name": "python_test", + "version": "0.0.1", + }], + "format": "image/jpeg", + "title": "Python Test Image", + "ingredients": [], + "assertions": [ + { + "label": "c2pa.actions", + "data": { + "actions": [ + { + "action": "c2pa.opened" + } + ] + } + } + ] + } + + # Create a temporary directory for the signed file + with tempfile.TemporaryDirectory() as temp_dir: + temp_file_path = os.path.join(temp_dir, "signed_test_file_no_embed.jpg") + + with open(self.testPath, "rb") as file: + builder = Builder(manifest_definition) + # Direct the Builder not to embed the manifest into the asset + builder.set_no_embed() + + + with open(temp_file_path, "wb") as temp_file: + manifest_data = builder.sign( + signer, "image/jpeg", file, temp_file) + + with Reader("image/jpeg", temp_file_path, manifest_data) as reader: + self.assertFalse(reader.is_embedded()) + + def test_reader_get_remote_url(self): + """Test the get_remote_url method returns correct values for embedded and remote manifests.""" + + # Test get_remote_url for file with embedded manifest (should return None) + with open(self.testPath, "rb") as file: + reader = Reader("image/jpeg", file) + self.assertIsNone(reader.get_remote_url()) + reader.close() + + # Test remote manifest using cloud.jpg fixture which has a remote URL + cloud_fixture_path = os.path.join(self.data_dir, "cloud.jpg") + with Reader("image/jpeg", cloud_fixture_path) as reader: + remote_url = reader.get_remote_url() + self.assertEqual(remote_url, "https://cai-manifests.adobe.com/manifests/adobe-urn-uuid-5f37e182-3687-462e-a7fb-573462780391") + self.assertFalse(reader.is_embedded()) + # TODO: Unskip once fixed configuration to read data is clarified # def test_read_cawg_data_file(self): # """Test reading C2PA metadata from C_with_CAWG_data.jpg file."""