c2pa-python 0.27.1__tar.gz → 0.28.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {c2pa_python-0.27.1/src/c2pa_python.egg-info → c2pa_python-0.28.0}/PKG-INFO +2 -1
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/README.md +1 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/pyproject.toml +1 -1
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa/__init__.py +4 -2
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa/c2pa.py +32 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa/lib.py +41 -11
- {c2pa_python-0.27.1 → c2pa_python-0.28.0/src/c2pa_python.egg-info}/PKG-INFO +2 -1
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/tests/test_unit_tests.py +327 -5
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/LICENSE-APACHE +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/LICENSE-MIT +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/MANIFEST.in +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/requirements.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/setup.cfg +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/setup.py +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa/build.py +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.27.1 → c2pa_python-0.28.0}/tests/test_unit_tests_threaded.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
|
|
5
5
|
Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
|
|
6
6
|
Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
|
|
@@ -57,6 +57,7 @@ import c2pa
|
|
|
57
57
|
|
|
58
58
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
59
59
|
|
|
60
|
+
- `examples/read.py` shows how to read and verify an asset with a C2PA manifest.
|
|
60
61
|
- `examples/sign.py` shows how to sign and verify an asset with a C2PA manifest.
|
|
61
62
|
- `examples/training.py` demonstrates how to add a "Do Not Train" assertion to an asset and verify it.
|
|
62
63
|
|
|
@@ -34,6 +34,7 @@ import c2pa
|
|
|
34
34
|
|
|
35
35
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
36
36
|
|
|
37
|
+
- `examples/read.py` shows how to read and verify an asset with a C2PA manifest.
|
|
37
38
|
- `examples/sign.py` shows how to sign and verify an asset with a C2PA manifest.
|
|
38
39
|
- `examples/training.py` demonstrates how to add a "Do Not Train" assertion to an asset and verify it.
|
|
39
40
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "c2pa-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.28.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
|
|
10
10
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -26,7 +26,8 @@ from .c2pa import (
|
|
|
26
26
|
Signer,
|
|
27
27
|
Stream,
|
|
28
28
|
sdk_version,
|
|
29
|
-
read_ingredient_file
|
|
29
|
+
read_ingredient_file,
|
|
30
|
+
load_settings
|
|
30
31
|
) # NOQA
|
|
31
32
|
|
|
32
33
|
# Re-export C2paError and its subclasses
|
|
@@ -39,5 +40,6 @@ __all__ = [
|
|
|
39
40
|
'Signer',
|
|
40
41
|
'Stream',
|
|
41
42
|
'sdk_version',
|
|
42
|
-
'read_ingredient_file'
|
|
43
|
+
'read_ingredient_file',
|
|
44
|
+
'load_settings'
|
|
43
45
|
]
|
|
@@ -41,6 +41,7 @@ _REQUIRED_FUNCTIONS = [
|
|
|
41
41
|
'c2pa_reader_from_manifest_data_and_stream',
|
|
42
42
|
'c2pa_reader_free',
|
|
43
43
|
'c2pa_reader_json',
|
|
44
|
+
'c2pa_reader_detailed_json',
|
|
44
45
|
'c2pa_reader_resource_to_stream',
|
|
45
46
|
'c2pa_builder_from_json',
|
|
46
47
|
'c2pa_builder_from_archive',
|
|
@@ -379,6 +380,9 @@ _setup_function(_lib.c2pa_reader_free, [ctypes.POINTER(C2paReader)], None)
|
|
|
379
380
|
_setup_function(
|
|
380
381
|
_lib.c2pa_reader_json, [
|
|
381
382
|
ctypes.POINTER(C2paReader)], ctypes.c_void_p)
|
|
383
|
+
_setup_function(
|
|
384
|
+
_lib.c2pa_reader_detailed_json, [
|
|
385
|
+
ctypes.POINTER(C2paReader)], ctypes.c_void_p)
|
|
382
386
|
_setup_function(_lib.c2pa_reader_resource_to_stream, [ctypes.POINTER(
|
|
383
387
|
C2paReader), ctypes.c_char_p, ctypes.POINTER(C2paStream)], ctypes.c_int64)
|
|
384
388
|
_setup_function(
|
|
@@ -1737,6 +1741,34 @@ class Reader:
|
|
|
1737
1741
|
self._manifest_json_str_cache = _convert_to_py_string(result)
|
|
1738
1742
|
return self._manifest_json_str_cache
|
|
1739
1743
|
|
|
1744
|
+
def detailed_json(self) -> str:
|
|
1745
|
+
"""Get the detailed JSON representation of the C2PA manifest store.
|
|
1746
|
+
|
|
1747
|
+
This method returns a more detailed JSON string than Reader.json(),
|
|
1748
|
+
providing additional information about the manifest structure.
|
|
1749
|
+
Note that the returned JSON by this method has a slightly different
|
|
1750
|
+
structure than the one returned by Reader.json().
|
|
1751
|
+
|
|
1752
|
+
Returns:
|
|
1753
|
+
A JSON string containing the detailed manifest store data.
|
|
1754
|
+
|
|
1755
|
+
Raises:
|
|
1756
|
+
C2paError: If there is an error reading the manifest data or if
|
|
1757
|
+
the Reader has been closed.
|
|
1758
|
+
"""
|
|
1759
|
+
|
|
1760
|
+
self._ensure_valid_state()
|
|
1761
|
+
|
|
1762
|
+
result = _lib.c2pa_reader_detailed_json(self._reader)
|
|
1763
|
+
|
|
1764
|
+
if result is None:
|
|
1765
|
+
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
1766
|
+
if error:
|
|
1767
|
+
raise C2paError(error)
|
|
1768
|
+
raise C2paError("Error during detailed manifest parsing in Reader")
|
|
1769
|
+
|
|
1770
|
+
return _convert_to_py_string(result)
|
|
1771
|
+
|
|
1740
1772
|
def get_active_manifest(self) -> Optional[dict]:
|
|
1741
1773
|
"""Get the active manifest from the manifest store.
|
|
1742
1774
|
|
|
@@ -106,7 +106,7 @@ def _get_platform_dir() -> str:
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def _load_single_library(lib_name: str,
|
|
109
|
-
search_paths: list[Path]) -> Optional[ctypes.CDLL]:
|
|
109
|
+
search_paths: list[Path]) -> tuple[Optional[ctypes.CDLL], list[tuple[Path, Exception]]]:
|
|
110
110
|
"""
|
|
111
111
|
Load a single library from the given search paths.
|
|
112
112
|
|
|
@@ -115,7 +115,7 @@ def _load_single_library(lib_name: str,
|
|
|
115
115
|
search_paths: List of paths to search for the library
|
|
116
116
|
|
|
117
117
|
Returns:
|
|
118
|
-
|
|
118
|
+
A tuple of (loaded library or None, list of (path, error) for files that were found but failed to load)
|
|
119
119
|
"""
|
|
120
120
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
121
121
|
logger.info(f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}")
|
|
@@ -123,6 +123,8 @@ def _load_single_library(lib_name: str,
|
|
|
123
123
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
124
124
|
logger.info(f"Current architecture: {current_arch}")
|
|
125
125
|
|
|
126
|
+
load_errors = []
|
|
127
|
+
|
|
126
128
|
for path in search_paths:
|
|
127
129
|
lib_path = path / lib_name
|
|
128
130
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
@@ -131,17 +133,20 @@ def _load_single_library(lib_name: str,
|
|
|
131
133
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
132
134
|
logger.info(f"Found library at: {lib_path}")
|
|
133
135
|
try:
|
|
134
|
-
return ctypes.CDLL(str(lib_path))
|
|
136
|
+
return ctypes.CDLL(str(lib_path)), []
|
|
135
137
|
except Exception as e:
|
|
136
138
|
error_msg = str(e)
|
|
139
|
+
load_errors.append((lib_path, e))
|
|
137
140
|
if "incompatible architecture" in error_msg:
|
|
138
141
|
logger.error(f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}")
|
|
139
142
|
logger.error(f"Error details: {error_msg}")
|
|
143
|
+
elif "GLIBC" in error_msg or "version" in error_msg.lower():
|
|
144
|
+
logger.error(f"Library dependency error at {lib_path}: {error_msg}")
|
|
140
145
|
else:
|
|
141
146
|
logger.error(f"Failed to load library from {lib_path}: {e}")
|
|
142
147
|
else:
|
|
143
148
|
logger.debug(f"Library not found at: {lib_path}")
|
|
144
|
-
return None
|
|
149
|
+
return None, load_errors
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
def _get_possible_search_paths() -> list[Path]:
|
|
@@ -250,19 +255,44 @@ def dynamically_load_library(
|
|
|
250
255
|
|
|
251
256
|
if lib_name:
|
|
252
257
|
# If specific library name is provided, only load that one
|
|
253
|
-
lib = _load_single_library(lib_name, possible_paths)
|
|
258
|
+
lib, load_errors = _load_single_library(lib_name, possible_paths)
|
|
254
259
|
if not lib:
|
|
255
260
|
platform_id = get_platform_identifier()
|
|
256
261
|
current_arch = _get_architecture()
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
|
|
263
|
+
if load_errors:
|
|
264
|
+
# Library files were found but failed to load
|
|
265
|
+
error_details = "\n".join([f" - {path}: {error}" for path, error in load_errors])
|
|
266
|
+
logger.error(f"Found {lib_name} but failed to load it. Errors encountered:")
|
|
267
|
+
logger.error(error_details)
|
|
268
|
+
logger.error(f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
269
|
+
raise RuntimeError(
|
|
270
|
+
f"Found {lib_name} at {len(load_errors)} location(s) but failed to load:\n{error_details}\n"
|
|
271
|
+
f"Platform: {platform_id}, Architecture: {current_arch}"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
# Library file was not found in any search path
|
|
275
|
+
logger.error(f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}")
|
|
276
|
+
logger.error(f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
277
|
+
raise RuntimeError(
|
|
278
|
+
f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})"
|
|
279
|
+
)
|
|
260
280
|
return lib
|
|
261
281
|
|
|
262
282
|
# Default path (no library name provided in the environment)
|
|
263
|
-
c2pa_lib = _load_single_library(c2pa_lib_name, possible_paths)
|
|
283
|
+
c2pa_lib, load_errors = _load_single_library(c2pa_lib_name, possible_paths)
|
|
264
284
|
if not c2pa_lib:
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
if load_errors:
|
|
286
|
+
# Library files were found but failed to load
|
|
287
|
+
error_details = "\n".join([f" - {path}: {error}" for path, error in load_errors])
|
|
288
|
+
logger.error(f"Found {c2pa_lib_name} but failed to load it. Errors encountered:")
|
|
289
|
+
logger.error(error_details)
|
|
290
|
+
raise RuntimeError(
|
|
291
|
+
f"Found {c2pa_lib_name} at {len(load_errors)} location(s) but failed to load:\n{error_details}"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
# Library file was not found in any search path
|
|
295
|
+
logger.error(f"Could not find {c2pa_lib_name} in any of the search paths: {[str(p) for p in possible_paths]}")
|
|
296
|
+
raise RuntimeError(f"Could not find {c2pa_lib_name} in any of the search paths")
|
|
267
297
|
|
|
268
298
|
return c2pa_lib
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
|
|
5
5
|
Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
|
|
6
6
|
Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
|
|
@@ -57,6 +57,7 @@ import c2pa
|
|
|
57
57
|
|
|
58
58
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
59
59
|
|
|
60
|
+
- `examples/read.py` shows how to read and verify an asset with a C2PA manifest.
|
|
60
61
|
- `examples/sign.py` shows how to sign and verify an asset with a C2PA manifest.
|
|
61
62
|
- `examples/training.py` demonstrates how to add a "Do Not Train" assertion to an asset and verify it.
|
|
62
63
|
|
|
@@ -64,6 +64,12 @@ class TestReader(unittest.TestCase):
|
|
|
64
64
|
json_data = reader.json()
|
|
65
65
|
self.assertIn(DEFAULT_TEST_FILE_NAME, json_data)
|
|
66
66
|
|
|
67
|
+
def test_stream_read_detailed(self):
|
|
68
|
+
with open(self.testPath, "rb") as file:
|
|
69
|
+
reader = Reader("image/jpeg", file)
|
|
70
|
+
json_data = reader.detailed_json()
|
|
71
|
+
self.assertIn(DEFAULT_TEST_FILE_NAME, json_data)
|
|
72
|
+
|
|
67
73
|
def test_get_active_manifest(self):
|
|
68
74
|
with open(self.testPath, "rb") as file:
|
|
69
75
|
reader = Reader("image/jpeg", file)
|
|
@@ -142,6 +148,13 @@ class TestReader(unittest.TestCase):
|
|
|
142
148
|
title = manifest_store["manifests"][manifest_store["active_manifest"]]["title"]
|
|
143
149
|
self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
|
|
144
150
|
|
|
151
|
+
def test_stream_read_detailed_and_parse(self):
|
|
152
|
+
with open(self.testPath, "rb") as file:
|
|
153
|
+
reader = Reader("image/jpeg", file)
|
|
154
|
+
manifest_store = json.loads(reader.detailed_json())
|
|
155
|
+
title = manifest_store["manifests"][manifest_store["active_manifest"]]["claim"]["dc:title"]
|
|
156
|
+
self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
|
|
157
|
+
|
|
145
158
|
def test_stream_read_string_stream(self):
|
|
146
159
|
with Reader(self.testPath) as reader:
|
|
147
160
|
json_data = reader.json()
|
|
@@ -564,7 +577,8 @@ class TestReader(unittest.TestCase):
|
|
|
564
577
|
"data": {
|
|
565
578
|
"actions": [
|
|
566
579
|
{
|
|
567
|
-
"action": "c2pa.
|
|
580
|
+
"action": "c2pa.created",
|
|
581
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
|
|
568
582
|
}
|
|
569
583
|
]
|
|
570
584
|
}
|
|
@@ -732,8 +746,8 @@ class TestBuilderWithSigner(unittest.TestCase):
|
|
|
732
746
|
"data": {
|
|
733
747
|
"actions": [
|
|
734
748
|
{
|
|
735
|
-
"action": "c2pa.
|
|
736
|
-
|
|
749
|
+
"action": "c2pa.created",
|
|
750
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
|
|
737
751
|
}
|
|
738
752
|
]
|
|
739
753
|
}
|
|
@@ -2214,8 +2228,9 @@ class TestBuilderWithSigner(unittest.TestCase):
|
|
|
2214
2228
|
"data": {
|
|
2215
2229
|
"actions": [
|
|
2216
2230
|
{
|
|
2217
|
-
"action": "c2pa.
|
|
2218
|
-
"description": "
|
|
2231
|
+
"action": "c2pa.created",
|
|
2232
|
+
"description": "Unicode: test",
|
|
2233
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
|
|
2219
2234
|
}
|
|
2220
2235
|
]
|
|
2221
2236
|
}
|
|
@@ -2911,6 +2926,313 @@ class TestBuilderWithSigner(unittest.TestCase):
|
|
|
2911
2926
|
# Reset settings
|
|
2912
2927
|
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
|
|
2913
2928
|
|
|
2929
|
+
def test_builder_opened_action_one_ingredient_no_auto_add(self):
|
|
2930
|
+
"""Test Builder with c2pa.opened action and one ingredient, following Adobe provenance patterns"""
|
|
2931
|
+
# Disable auto-added actions
|
|
2932
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
2933
|
+
|
|
2934
|
+
# Instance IDs for linking ingredients and actions
|
|
2935
|
+
# This can be any unique id so the ingredient can be uniquely identified and linked to the action
|
|
2936
|
+
parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
|
|
2937
|
+
|
|
2938
|
+
manifestDefinition = {
|
|
2939
|
+
"claim_generator_info": [{
|
|
2940
|
+
"name": "Python CAI test",
|
|
2941
|
+
"version": "3.14.16"
|
|
2942
|
+
}],
|
|
2943
|
+
"title": "A title for the provenance test",
|
|
2944
|
+
"ingredients": [
|
|
2945
|
+
# The parent ingredient will be added through add_ingredient
|
|
2946
|
+
# And a properly crafted manifest json so they link
|
|
2947
|
+
],
|
|
2948
|
+
"assertions": [
|
|
2949
|
+
{
|
|
2950
|
+
"label": "c2pa.actions.v2",
|
|
2951
|
+
"data": {
|
|
2952
|
+
"actions": [
|
|
2953
|
+
{
|
|
2954
|
+
"action": "c2pa.opened",
|
|
2955
|
+
"softwareAgent": {
|
|
2956
|
+
"name": "Opened asset",
|
|
2957
|
+
},
|
|
2958
|
+
"parameters": {
|
|
2959
|
+
"ingredientIds": [
|
|
2960
|
+
parent_ingredient_id
|
|
2961
|
+
]
|
|
2962
|
+
},
|
|
2963
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
|
|
2964
|
+
}
|
|
2965
|
+
]
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
]
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
# The ingredient json for the opened action needs to match the instance_id in the manifestDefinition
|
|
2972
|
+
# Aka the unique parent_ingredient_id we rely on for linking
|
|
2973
|
+
ingredient_json = {
|
|
2974
|
+
"relationship": "parentOf",
|
|
2975
|
+
"instance_id": parent_ingredient_id
|
|
2976
|
+
}
|
|
2977
|
+
# An opened ingredient is always a parent, and there can only be exactly one parent ingredient
|
|
2978
|
+
|
|
2979
|
+
# Read the input file (A.jpg will be signed)
|
|
2980
|
+
with open(self.testPath2, "rb") as test_file:
|
|
2981
|
+
file_content = test_file.read()
|
|
2982
|
+
|
|
2983
|
+
builder = Builder.from_json(manifestDefinition)
|
|
2984
|
+
|
|
2985
|
+
# Add C.jpg as the parent "opened" ingredient
|
|
2986
|
+
with open(self.testPath, 'rb') as f:
|
|
2987
|
+
builder.add_ingredient(ingredient_json, "image/jpeg", f)
|
|
2988
|
+
|
|
2989
|
+
output_buffer = io.BytesIO(bytearray())
|
|
2990
|
+
builder.sign(
|
|
2991
|
+
self.signer,
|
|
2992
|
+
"image/jpeg",
|
|
2993
|
+
io.BytesIO(file_content),
|
|
2994
|
+
output_buffer)
|
|
2995
|
+
output_buffer.seek(0)
|
|
2996
|
+
|
|
2997
|
+
# Read and verify the manifest
|
|
2998
|
+
reader = Reader("image/jpeg", output_buffer)
|
|
2999
|
+
json_data = reader.json()
|
|
3000
|
+
manifest_data = json.loads(json_data)
|
|
3001
|
+
|
|
3002
|
+
# Verify the ingredient instance ID is present
|
|
3003
|
+
self.assertIn(parent_ingredient_id, json_data)
|
|
3004
|
+
|
|
3005
|
+
# Verify c2pa.opened action is present
|
|
3006
|
+
self.assertIn("c2pa.opened", json_data)
|
|
3007
|
+
|
|
3008
|
+
builder.close()
|
|
3009
|
+
|
|
3010
|
+
# Make sure settings are put back to the common test defaults
|
|
3011
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
3012
|
+
|
|
3013
|
+
def test_builder_one_opened_one_placed_action_no_auto_add(self):
|
|
3014
|
+
"""Test Builder with c2pa.opened action where asset is its own parent ingredient"""
|
|
3015
|
+
# Disable auto-added actions
|
|
3016
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
3017
|
+
|
|
3018
|
+
# Instance IDs for linking ingredients and actions,
|
|
3019
|
+
# need to be unique even if the same binary file is used, so ingredients link properly to actions
|
|
3020
|
+
parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
|
|
3021
|
+
placed_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
|
|
3022
|
+
|
|
3023
|
+
manifestDefinition = {
|
|
3024
|
+
"claim_generator_info": [{
|
|
3025
|
+
"name": "Python CAI test",
|
|
3026
|
+
"version": "0.2.942"
|
|
3027
|
+
}],
|
|
3028
|
+
"title": "A title for the provenance test",
|
|
3029
|
+
"ingredients": [
|
|
3030
|
+
# The parent ingredient will be added through add_ingredient
|
|
3031
|
+
{
|
|
3032
|
+
# Represents the bubbled up AI asset/ingredient
|
|
3033
|
+
"format": "jpeg",
|
|
3034
|
+
"relationship": "componentOf",
|
|
3035
|
+
# Instance ID must be generated to match what is in parameters ingredientIds array
|
|
3036
|
+
"instance_id": placed_ingredient_id,
|
|
3037
|
+
}
|
|
3038
|
+
],
|
|
3039
|
+
"assertions": [
|
|
3040
|
+
{
|
|
3041
|
+
"label": "c2pa.actions.v2",
|
|
3042
|
+
"data": {
|
|
3043
|
+
"actions": [
|
|
3044
|
+
{
|
|
3045
|
+
"action": "c2pa.opened",
|
|
3046
|
+
"softwareAgent": {
|
|
3047
|
+
"name": "Opened asset",
|
|
3048
|
+
},
|
|
3049
|
+
"parameters": {
|
|
3050
|
+
"ingredientIds": [
|
|
3051
|
+
parent_ingredient_id
|
|
3052
|
+
]
|
|
3053
|
+
},
|
|
3054
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
|
|
3055
|
+
},
|
|
3056
|
+
{
|
|
3057
|
+
"action": "c2pa.placed",
|
|
3058
|
+
"softwareAgent": {
|
|
3059
|
+
"name": "Placed asset",
|
|
3060
|
+
},
|
|
3061
|
+
"parameters": {
|
|
3062
|
+
"ingredientIds": [
|
|
3063
|
+
placed_ingredient_id
|
|
3064
|
+
]
|
|
3065
|
+
},
|
|
3066
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
|
|
3067
|
+
}
|
|
3068
|
+
]
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
]
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
# The ingredient json for the opened action needs to match the instance_id in the manifestDefinition for c2pa.opened
|
|
3075
|
+
# So that ingredients can link together.
|
|
3076
|
+
ingredient_json = {
|
|
3077
|
+
"relationship": "parentOf",
|
|
3078
|
+
"when": "2025-08-07T18:01:55.934Z",
|
|
3079
|
+
"instance_id": parent_ingredient_id
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
# Read the input file (A.jpg will be signed)
|
|
3083
|
+
with open(self.testPath2, "rb") as test_file:
|
|
3084
|
+
file_content = test_file.read()
|
|
3085
|
+
|
|
3086
|
+
builder = Builder.from_json(manifestDefinition)
|
|
3087
|
+
|
|
3088
|
+
# An asset can be its own parent ingredient!
|
|
3089
|
+
# We add A.jpg as its own parent ingredient
|
|
3090
|
+
with open(self.testPath2, 'rb') as f:
|
|
3091
|
+
builder.add_ingredient(ingredient_json, "image/jpeg", f)
|
|
3092
|
+
|
|
3093
|
+
output_buffer = io.BytesIO(bytearray())
|
|
3094
|
+
builder.sign(
|
|
3095
|
+
self.signer,
|
|
3096
|
+
"image/jpeg",
|
|
3097
|
+
io.BytesIO(file_content),
|
|
3098
|
+
output_buffer)
|
|
3099
|
+
output_buffer.seek(0)
|
|
3100
|
+
|
|
3101
|
+
# Read and verify the manifest
|
|
3102
|
+
reader = Reader("image/jpeg", output_buffer)
|
|
3103
|
+
json_data = reader.json()
|
|
3104
|
+
manifest_data = json.loads(json_data)
|
|
3105
|
+
|
|
3106
|
+
# Verify both ingredient instance IDs are present
|
|
3107
|
+
self.assertIn(parent_ingredient_id, json_data)
|
|
3108
|
+
self.assertIn(placed_ingredient_id, json_data)
|
|
3109
|
+
|
|
3110
|
+
# Verify both actions are present
|
|
3111
|
+
self.assertIn("c2pa.opened", json_data)
|
|
3112
|
+
self.assertIn("c2pa.placed", json_data)
|
|
3113
|
+
|
|
3114
|
+
builder.close()
|
|
3115
|
+
|
|
3116
|
+
# Make sure settings are put back to the common test defaults
|
|
3117
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
3118
|
+
|
|
3119
|
+
def test_builder_opened_action_multiple_ingredient_no_auto_add(self):
|
|
3120
|
+
"""Test Builder with c2pa.opened and c2pa.placed actions with multiple ingredients"""
|
|
3121
|
+
# Disable auto-added actions, as what we are doing here can confuse auto-placements
|
|
3122
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
3123
|
+
|
|
3124
|
+
# Instance IDs for linking ingredients and actions
|
|
3125
|
+
# With multiple ingredients, we need multiple different unique ids so they each link properly
|
|
3126
|
+
parent_ingredient_id = "xmp:iid:a965983b-36fb-445a-aa80-a2d911dcc53c"
|
|
3127
|
+
placed_ingredient_1_id = "xmp:iid:a965983b-36fb-445a-aa80-f3f800ebe42b"
|
|
3128
|
+
placed_ingredient_2_id = "xmp:iid:a965983b-36fb-445a-aa80-f2d712acd14c"
|
|
3129
|
+
|
|
3130
|
+
manifestDefinition = {
|
|
3131
|
+
"claim_generator_info": [{
|
|
3132
|
+
"name": "Python CAI test",
|
|
3133
|
+
"version": "0.2.942"
|
|
3134
|
+
}],
|
|
3135
|
+
"title": "A title for the provenance test with multiple ingredients",
|
|
3136
|
+
"ingredients": [
|
|
3137
|
+
# More ingredients will be added using add_ingredient
|
|
3138
|
+
{
|
|
3139
|
+
"format": "jpeg",
|
|
3140
|
+
"relationship": "componentOf",
|
|
3141
|
+
# Instance ID must be generated to match what is in parameters ingredientIds array
|
|
3142
|
+
"instance_id": placed_ingredient_1_id,
|
|
3143
|
+
}
|
|
3144
|
+
],
|
|
3145
|
+
"assertions": [
|
|
3146
|
+
{
|
|
3147
|
+
"label": "c2pa.actions.v2",
|
|
3148
|
+
"data": {
|
|
3149
|
+
"actions": [
|
|
3150
|
+
{
|
|
3151
|
+
"action": "c2pa.opened",
|
|
3152
|
+
"softwareAgent": {
|
|
3153
|
+
"name": "A parent opened asset",
|
|
3154
|
+
},
|
|
3155
|
+
"parameters": {
|
|
3156
|
+
"ingredientIds": [
|
|
3157
|
+
parent_ingredient_id
|
|
3158
|
+
]
|
|
3159
|
+
},
|
|
3160
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
|
|
3161
|
+
},
|
|
3162
|
+
{
|
|
3163
|
+
"action": "c2pa.placed",
|
|
3164
|
+
"softwareAgent": {
|
|
3165
|
+
"name": "Component placed assets",
|
|
3166
|
+
},
|
|
3167
|
+
"parameters": {
|
|
3168
|
+
"ingredientIds": [
|
|
3169
|
+
placed_ingredient_1_id,
|
|
3170
|
+
placed_ingredient_2_id
|
|
3171
|
+
]
|
|
3172
|
+
},
|
|
3173
|
+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia"
|
|
3174
|
+
}
|
|
3175
|
+
]
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
]
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
# The ingredient json for the opened action needs to match the instance_id in the manifestDefinition,
|
|
3182
|
+
# so that ingredients properly link with their action
|
|
3183
|
+
ingredient_json_parent = {
|
|
3184
|
+
"relationship": "parentOf",
|
|
3185
|
+
"instance_id": parent_ingredient_id
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
# The ingredient json for the placed action needs to match the instance_id in the manifestDefinition,
|
|
3189
|
+
# so that ingredients properly link with their action
|
|
3190
|
+
ingredient_json_placed = {
|
|
3191
|
+
"relationship": "componentOf",
|
|
3192
|
+
"instance_id": placed_ingredient_2_id
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
# Read the input file (A.jpg will be signed)
|
|
3196
|
+
with open(self.testPath2, "rb") as test_file:
|
|
3197
|
+
file_content = test_file.read()
|
|
3198
|
+
|
|
3199
|
+
builder = Builder.from_json(manifestDefinition)
|
|
3200
|
+
|
|
3201
|
+
# Add C.jpg as the parent ingredient (for c2pa.opened, it's the opened asset)
|
|
3202
|
+
with open(self.testPath, 'rb') as f1:
|
|
3203
|
+
builder.add_ingredient(ingredient_json_parent, "image/jpeg", f1)
|
|
3204
|
+
|
|
3205
|
+
# Add cloud.jpg as another placed ingredient (for instance, added on the opened asset)
|
|
3206
|
+
with open(self.testPath4, 'rb') as f2:
|
|
3207
|
+
builder.add_ingredient(ingredient_json_placed, "image/jpeg", f2)
|
|
3208
|
+
|
|
3209
|
+
output_buffer = io.BytesIO(bytearray())
|
|
3210
|
+
builder.sign(
|
|
3211
|
+
self.signer,
|
|
3212
|
+
"image/jpeg",
|
|
3213
|
+
io.BytesIO(file_content),
|
|
3214
|
+
output_buffer)
|
|
3215
|
+
output_buffer.seek(0)
|
|
3216
|
+
|
|
3217
|
+
# Read and verify the manifest
|
|
3218
|
+
reader = Reader("image/jpeg", output_buffer)
|
|
3219
|
+
json_data = reader.json()
|
|
3220
|
+
manifest_data = json.loads(json_data)
|
|
3221
|
+
|
|
3222
|
+
# Verify all ingredient instance IDs are present
|
|
3223
|
+
self.assertIn(parent_ingredient_id, json_data)
|
|
3224
|
+
self.assertIn(placed_ingredient_1_id, json_data)
|
|
3225
|
+
self.assertIn(placed_ingredient_2_id, json_data)
|
|
3226
|
+
|
|
3227
|
+
# Verify both actions are present
|
|
3228
|
+
self.assertIn("c2pa.opened", json_data)
|
|
3229
|
+
self.assertIn("c2pa.placed", json_data)
|
|
3230
|
+
|
|
3231
|
+
builder.close()
|
|
3232
|
+
|
|
3233
|
+
# Make sure settings are put back to the common test defaults
|
|
3234
|
+
load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
|
|
3235
|
+
|
|
2914
3236
|
|
|
2915
3237
|
class TestStream(unittest.TestCase):
|
|
2916
3238
|
def setUp(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|