Skip to content

Commit 4593aac

Browse files
authored
feat: Add bindings for is_embedded and get_remote_url (#178)
* fix: Add some more APIs * fix: Add some more APIs 2 * fix: Format * fix: Cleaning up * fix: Prepare release with version bump
1 parent 23f791c commit 4593aac

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "c2pa-python"
7-
version = "0.24.1"
7+
version = "0.25.0"
88
requires-python = ">=3.10"
99
description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
1010
readme = { file = "README.md", content-type = "text/markdown" }

src/c2pa/c2pa.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
'c2pa_free_string_array',
6565
'c2pa_reader_supported_mime_types',
6666
'c2pa_builder_supported_mime_types',
67+
'c2pa_reader_is_embedded',
68+
'c2pa_reader_remote_url',
6769
]
6870

6971
# TODO Bindings:
@@ -369,6 +371,16 @@ def _setup_function(func, argtypes, restype=None):
369371
[ctypes.POINTER(ctypes.c_size_t)],
370372
ctypes.POINTER(ctypes.c_char_p)
371373
)
374+
_setup_function(
375+
_lib.c2pa_reader_is_embedded,
376+
[ctypes.POINTER(C2paReader)],
377+
ctypes.c_bool
378+
)
379+
_setup_function(
380+
_lib.c2pa_reader_remote_url,
381+
[ctypes.POINTER(C2paReader)],
382+
ctypes.c_void_p
383+
)
372384

373385
# Set up Builder function prototypes
374386
_setup_function(
@@ -1662,6 +1674,45 @@ def resource_to_stream(self, uri: str, stream: Any) -> int:
16621674

16631675
return result
16641676

1677+
def is_embedded(self) -> bool:
1678+
"""Check if the reader was created from an embedded manifest.
1679+
This method determines whether the C2PA manifest is embedded directly
1680+
in the asset file or stored remotely.
1681+
Returns:
1682+
True if the reader was created from an embedded manifest,
1683+
False if it was created from a remote manifest
1684+
Raises:
1685+
C2paError: If there was an error checking the embedded status
1686+
"""
1687+
self._ensure_valid_state()
1688+
1689+
result = _lib.c2pa_reader_is_embedded(self._reader)
1690+
1691+
return bool(result)
1692+
1693+
def get_remote_url(self) -> Optional[str]:
1694+
"""Get the remote URL of the manifest if it was obtained remotely.
1695+
This method returns the URL from which the C2PA manifest was fetched
1696+
if the reader was created from a remote manifest. If the manifest
1697+
is embedded in the asset, this will return None.
1698+
Returns:
1699+
The remote URL as a string if the manifest was obtained remotely,
1700+
None if the manifest is embedded or no remote URL is available
1701+
Raises:
1702+
C2paError: If there was an error getting the remote URL
1703+
"""
1704+
self._ensure_valid_state()
1705+
1706+
result = _lib.c2pa_reader_remote_url(self._reader)
1707+
1708+
if result is None:
1709+
# No remote URL set (manifest is embedded)
1710+
return None
1711+
1712+
# Convert the C string to Python string
1713+
url_str = _convert_to_py_string(result)
1714+
return url_str
1715+
16651716

16661717
class Signer:
16671718
"""High-level wrapper for C2PA Signer operations."""

tests/test_unit_tests.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,94 @@ def test_reader_state_with_invalid_native_pointer(self):
342342
with self.assertRaises(Error):
343343
reader.json()
344344

345+
def test_reader_is_embedded(self):
346+
"""Test the is_embedded method returns correct values for embedded and remote manifests."""
347+
348+
# Test with a fixture which has an embedded manifest
349+
with open(self.testPath, "rb") as file:
350+
reader = Reader("image/jpeg", file)
351+
self.assertTrue(reader.is_embedded())
352+
reader.close()
353+
354+
# Test with cloud.jpg fixture which has a remote manifest (not embedded)
355+
cloud_fixture_path = os.path.join(self.data_dir, "cloud.jpg")
356+
with Reader("image/jpeg", cloud_fixture_path) as reader:
357+
self.assertFalse(reader.is_embedded())
358+
359+
def test_sign_and_read_is_not_embedded(self):
360+
"""Test the is_embedded method returns correct values for remote manifests."""
361+
362+
with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
363+
certs = cert_file.read()
364+
with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
365+
key = key_file.read()
366+
367+
# Create signer info and signer
368+
signer_info = C2paSignerInfo(
369+
alg=b"es256",
370+
sign_cert=certs,
371+
private_key=key,
372+
ta_url=b"http://timestamp.digicert.com"
373+
)
374+
signer = Signer.from_info(signer_info)
375+
376+
# Define a simple manifest
377+
manifest_definition = {
378+
"claim_generator": "python_test",
379+
"claim_generator_info": [{
380+
"name": "python_test",
381+
"version": "0.0.1",
382+
}],
383+
"format": "image/jpeg",
384+
"title": "Python Test Image",
385+
"ingredients": [],
386+
"assertions": [
387+
{
388+
"label": "c2pa.actions",
389+
"data": {
390+
"actions": [
391+
{
392+
"action": "c2pa.opened"
393+
}
394+
]
395+
}
396+
}
397+
]
398+
}
399+
400+
# Create a temporary directory for the signed file
401+
with tempfile.TemporaryDirectory() as temp_dir:
402+
temp_file_path = os.path.join(temp_dir, "signed_test_file_no_embed.jpg")
403+
404+
with open(self.testPath, "rb") as file:
405+
builder = Builder(manifest_definition)
406+
# Direct the Builder not to embed the manifest into the asset
407+
builder.set_no_embed()
408+
409+
410+
with open(temp_file_path, "wb") as temp_file:
411+
manifest_data = builder.sign(
412+
signer, "image/jpeg", file, temp_file)
413+
414+
with Reader("image/jpeg", temp_file_path, manifest_data) as reader:
415+
self.assertFalse(reader.is_embedded())
416+
417+
def test_reader_get_remote_url(self):
418+
"""Test the get_remote_url method returns correct values for embedded and remote manifests."""
419+
420+
# Test get_remote_url for file with embedded manifest (should return None)
421+
with open(self.testPath, "rb") as file:
422+
reader = Reader("image/jpeg", file)
423+
self.assertIsNone(reader.get_remote_url())
424+
reader.close()
425+
426+
# Test remote manifest using cloud.jpg fixture which has a remote URL
427+
cloud_fixture_path = os.path.join(self.data_dir, "cloud.jpg")
428+
with Reader("image/jpeg", cloud_fixture_path) as reader:
429+
remote_url = reader.get_remote_url()
430+
self.assertEqual(remote_url, "https://cai-manifests.adobe.com/manifests/adobe-urn-uuid-5f37e182-3687-462e-a7fb-573462780391")
431+
self.assertFalse(reader.is_embedded())
432+
345433
# TODO: Unskip once fixed configuration to read data is clarified
346434
# def test_read_cawg_data_file(self):
347435
# """Test reading C2PA metadata from C_with_CAWG_data.jpg file."""

0 commit comments

Comments
 (0)