Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
51 changes: 51 additions & 0 deletions src/c2pa/c2pa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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."""
Expand Down
88 changes: 88 additions & 0 deletions tests/test_unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down