c2pa-python 0.32.11__tar.gz → 0.32.13__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.32.11/src/c2pa_python.egg-info → c2pa_python-0.32.13}/PKG-INFO +43 -1
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/README.md +42 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/pyproject.toml +1 -1
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/c2pa.py +130 -93
- {c2pa_python-0.32.11 → c2pa_python-0.32.13/src/c2pa_python.egg-info}/PKG-INFO +43 -1
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/tests/test_unit_tests.py +16 -1
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/LICENSE-APACHE +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/LICENSE-MIT +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/MANIFEST.in +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/requirements.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/setup.cfg +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/setup.py +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/build.py +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/lib.py +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.32.11 → c2pa_python-0.32.13}/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.32.
|
|
3
|
+
Version: 0.32.13
|
|
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>
|
|
@@ -69,6 +69,48 @@ To use the module in Python code, import the module like this:
|
|
|
69
69
|
import c2pa
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Building from local c2pa-rs sources
|
|
73
|
+
|
|
74
|
+
### Using a virtual environment with local builds
|
|
75
|
+
|
|
76
|
+
The `make` targets honor an active virtualenv. Create a virtual environment `./.venv` and activate it before running them so commands use the project interpreter rather than the global Python interpreter:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
make create-venv && source .venv/bin/activate
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Build steps
|
|
83
|
+
|
|
84
|
+
By default the build downloads a prebuilt native library from a [c2pa-rs](https://github.com/contentauth/c2pa-rs) release. To test the Python bindings against a local, unreleased c2pa-rs checkout, you can instead build the native library from source.
|
|
85
|
+
|
|
86
|
+
Prerequisites:
|
|
87
|
+
|
|
88
|
+
- A local clone of [c2pa-rs](https://github.com/contentauth/c2pa-rs).
|
|
89
|
+
- The [Rust toolchain](https://rust-lang.org/tools/install/) (`cargo` on your `PATH`).
|
|
90
|
+
|
|
91
|
+
Point `C2PA_RS_PATH` at your c2pa-rs checkout and run the `build-from-source` target:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
export C2PA_RS_PATH=/path/to/c2pa-rs
|
|
95
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. The release profile is used by default; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH EXTRA_BUILD_ARGS="--debug"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Note on targets for macOS
|
|
105
|
+
|
|
106
|
+
On macOS this produces a universal (arm64+x86_64) library by default, which requires both Rust targets:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To build a single-architecture library instead, set `C2PA_LIBS_PLATFORM` to a specific platform (for example `aarch64-apple-darwin`).
|
|
113
|
+
|
|
72
114
|
## Examples
|
|
73
115
|
|
|
74
116
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
@@ -46,6 +46,48 @@ To use the module in Python code, import the module like this:
|
|
|
46
46
|
import c2pa
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## Building from local c2pa-rs sources
|
|
50
|
+
|
|
51
|
+
### Using a virtual environment with local builds
|
|
52
|
+
|
|
53
|
+
The `make` targets honor an active virtualenv. Create a virtual environment `./.venv` and activate it before running them so commands use the project interpreter rather than the global Python interpreter:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
make create-venv && source .venv/bin/activate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Build steps
|
|
60
|
+
|
|
61
|
+
By default the build downloads a prebuilt native library from a [c2pa-rs](https://github.com/contentauth/c2pa-rs) release. To test the Python bindings against a local, unreleased c2pa-rs checkout, you can instead build the native library from source.
|
|
62
|
+
|
|
63
|
+
Prerequisites:
|
|
64
|
+
|
|
65
|
+
- A local clone of [c2pa-rs](https://github.com/contentauth/c2pa-rs).
|
|
66
|
+
- The [Rust toolchain](https://rust-lang.org/tools/install/) (`cargo` on your `PATH`).
|
|
67
|
+
|
|
68
|
+
Point `C2PA_RS_PATH` at your c2pa-rs checkout and run the `build-from-source` target:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
export C2PA_RS_PATH=/path/to/c2pa-rs
|
|
72
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. The release profile is used by default; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH EXTRA_BUILD_ARGS="--debug"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Note on targets for macOS
|
|
82
|
+
|
|
83
|
+
On macOS this produces a universal (arm64+x86_64) library by default, which requires both Rust targets:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To build a single-architecture library instead, set `C2PA_LIBS_PLATFORM` to a specific platform (for example `aarch64-apple-darwin`).
|
|
90
|
+
|
|
49
91
|
## Examples
|
|
50
92
|
|
|
51
93
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "c2pa-python"
|
|
7
|
-
version = "0.32.
|
|
7
|
+
version = "0.32.13"
|
|
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" }
|
|
@@ -253,7 +253,8 @@ class ManagedResource:
|
|
|
253
253
|
if self._lifecycle_state != LifecycleState.ACTIVE:
|
|
254
254
|
raise C2paError(f"{name} is not properly initialized")
|
|
255
255
|
if not self._handle:
|
|
256
|
-
raise C2paError(
|
|
256
|
+
raise C2paError(
|
|
257
|
+
f"{name} has an invalid internal state (active but no handle)")
|
|
257
258
|
_clear_error_state()
|
|
258
259
|
|
|
259
260
|
def _release(self):
|
|
@@ -1011,7 +1012,11 @@ def _parse_operation_result_for_error(
|
|
|
1011
1012
|
return None
|
|
1012
1013
|
|
|
1013
1014
|
|
|
1014
|
-
def _check_ffi_operation_result(
|
|
1015
|
+
def _check_ffi_operation_result(
|
|
1016
|
+
result,
|
|
1017
|
+
fallback_msg,
|
|
1018
|
+
*,
|
|
1019
|
+
check=lambda r: not r):
|
|
1015
1020
|
"""Check an FFI native call result and raise C2paError if it indicates failure.
|
|
1016
1021
|
|
|
1017
1022
|
Args:
|
|
@@ -1036,7 +1041,8 @@ def _check_ffi_operation_result(result, fallback_msg, *, check=lambda r: not r):
|
|
|
1036
1041
|
return result
|
|
1037
1042
|
|
|
1038
1043
|
|
|
1039
|
-
def _to_utf8_bytes(data: Union[str, dict],
|
|
1044
|
+
def _to_utf8_bytes(data: Union[str, dict],
|
|
1045
|
+
error_context: str = "input") -> bytes:
|
|
1040
1046
|
"""Convert a string or dict to UTF-8 bytes.
|
|
1041
1047
|
|
|
1042
1048
|
If data is a dict, it is serialized to JSON first.
|
|
@@ -1059,8 +1065,8 @@ def _to_utf8_bytes(data: Union[str, dict], error_context: str = "input") -> byte
|
|
|
1059
1065
|
raise C2paError.Json(f"Failed to serialize {error_context}: {e}")
|
|
1060
1066
|
if not isinstance(data, str):
|
|
1061
1067
|
raise C2paError.Encoding(
|
|
1062
|
-
f"Expected str or dict for {error_context},
|
|
1063
|
-
|
|
1068
|
+
f"Expected str or dict for {error_context}, "
|
|
1069
|
+
f"got {type(data).__name__}")
|
|
1064
1070
|
try:
|
|
1065
1071
|
return data.encode('utf-8')
|
|
1066
1072
|
except UnicodeError as e:
|
|
@@ -1146,7 +1152,10 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None:
|
|
|
1146
1152
|
raise C2paError(f"Failed to encode settings to UTF-8: {e}")
|
|
1147
1153
|
|
|
1148
1154
|
result = _lib.c2pa_load_settings(settings_bytes, format_bytes)
|
|
1149
|
-
_check_ffi_operation_result(
|
|
1155
|
+
_check_ffi_operation_result(
|
|
1156
|
+
result,
|
|
1157
|
+
"Error loading settings",
|
|
1158
|
+
check=lambda r: r != 0)
|
|
1150
1159
|
|
|
1151
1160
|
return result
|
|
1152
1161
|
|
|
@@ -1219,7 +1228,8 @@ def read_ingredient_file(
|
|
|
1219
1228
|
result = _lib.c2pa_read_ingredient_file(
|
|
1220
1229
|
container._path_str, container._data_dir_str)
|
|
1221
1230
|
|
|
1222
|
-
_check_ffi_operation_result(
|
|
1231
|
+
_check_ffi_operation_result(
|
|
1232
|
+
result, "Error reading ingredient file {}".format(path))
|
|
1223
1233
|
|
|
1224
1234
|
return _convert_to_py_string(result)
|
|
1225
1235
|
|
|
@@ -1258,7 +1268,8 @@ def read_file(path: Union[str, Path],
|
|
|
1258
1268
|
container._data_dir_str = str(data_dir).encode('utf-8')
|
|
1259
1269
|
|
|
1260
1270
|
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
|
|
1261
|
-
_check_ffi_operation_result(
|
|
1271
|
+
_check_ffi_operation_result(
|
|
1272
|
+
result, "Error during read of manifest from file {}".format(path))
|
|
1262
1273
|
|
|
1263
1274
|
return _convert_to_py_string(result)
|
|
1264
1275
|
|
|
@@ -1419,13 +1430,17 @@ class Settings(ManagedResource):
|
|
|
1419
1430
|
apply settings to Reader/Builder operations.
|
|
1420
1431
|
"""
|
|
1421
1432
|
|
|
1422
|
-
|
|
1423
1433
|
def __init__(self):
|
|
1424
1434
|
"""Create new Settings with default values."""
|
|
1425
1435
|
super().__init__()
|
|
1426
1436
|
|
|
1427
1437
|
ptr = _lib.c2pa_settings_new()
|
|
1428
|
-
|
|
1438
|
+
try:
|
|
1439
|
+
_check_ffi_operation_result(ptr, "Failed to create Settings")
|
|
1440
|
+
except Exception:
|
|
1441
|
+
if ptr:
|
|
1442
|
+
ManagedResource._free_native_ptr(ptr)
|
|
1443
|
+
raise
|
|
1429
1444
|
|
|
1430
1445
|
self._handle = ptr
|
|
1431
1446
|
self._lifecycle_state = LifecycleState.ACTIVE
|
|
@@ -1511,7 +1526,6 @@ class Settings(ManagedResource):
|
|
|
1511
1526
|
return self._handle
|
|
1512
1527
|
|
|
1513
1528
|
|
|
1514
|
-
|
|
1515
1529
|
class ContextBuilder:
|
|
1516
1530
|
"""Fluent builder for Context.
|
|
1517
1531
|
|
|
@@ -1569,7 +1583,6 @@ class Context(ManagedResource, ContextProvider):
|
|
|
1569
1583
|
used directly again after that.
|
|
1570
1584
|
"""
|
|
1571
1585
|
|
|
1572
|
-
|
|
1573
1586
|
def __init__(
|
|
1574
1587
|
self,
|
|
1575
1588
|
settings: Optional['Settings'] = None,
|
|
@@ -1713,7 +1726,6 @@ class Context(ManagedResource, ContextProvider):
|
|
|
1713
1726
|
return self._handle
|
|
1714
1727
|
|
|
1715
1728
|
|
|
1716
|
-
|
|
1717
1729
|
class Stream:
|
|
1718
1730
|
# Class-level somewhat atomic counter for generating
|
|
1719
1731
|
# unique stream IDs (useful for tracing streams usage in debug)
|
|
@@ -1807,7 +1819,17 @@ class Stream:
|
|
|
1807
1819
|
if not data or length <= 0:
|
|
1808
1820
|
return -1
|
|
1809
1821
|
|
|
1810
|
-
|
|
1822
|
+
stream = self._file_like_stream
|
|
1823
|
+
readinto = getattr(stream, "readinto", None)
|
|
1824
|
+
if readinto is not None:
|
|
1825
|
+
# Most streams have readinto
|
|
1826
|
+
buf = (ctypes.c_char * length).from_address(
|
|
1827
|
+
ctypes.addressof(data.contents))
|
|
1828
|
+
n = readinto(buf)
|
|
1829
|
+
return n if n else 0
|
|
1830
|
+
|
|
1831
|
+
# Fallback for streams without readinto.
|
|
1832
|
+
buffer = stream.read(length)
|
|
1811
1833
|
if not buffer: # EOF
|
|
1812
1834
|
return 0
|
|
1813
1835
|
|
|
@@ -1841,8 +1863,10 @@ class Stream:
|
|
|
1841
1863
|
if not self._initialized or self._closed:
|
|
1842
1864
|
return -1
|
|
1843
1865
|
try:
|
|
1844
|
-
|
|
1845
|
-
return
|
|
1866
|
+
# Fall back to tell() only for stream objects that do not
|
|
1867
|
+
# return the new absolute position and return None.
|
|
1868
|
+
pos = file_stream.seek(offset, whence)
|
|
1869
|
+
return pos if pos is not None else file_stream.tell()
|
|
1846
1870
|
except Exception:
|
|
1847
1871
|
return -1
|
|
1848
1872
|
|
|
@@ -2115,7 +2139,6 @@ class Reader(ManagedResource):
|
|
|
2115
2139
|
Where `output` is either an in-memory stream or an opened file.
|
|
2116
2140
|
"""
|
|
2117
2141
|
|
|
2118
|
-
|
|
2119
2142
|
# Supported mimetypes cache
|
|
2120
2143
|
_supported_mime_types_cache = None
|
|
2121
2144
|
|
|
@@ -2146,8 +2169,7 @@ class Reader(ManagedResource):
|
|
|
2146
2169
|
C2paError: If there was an error retrieving the MIME types
|
|
2147
2170
|
"""
|
|
2148
2171
|
result, cls._supported_mime_types_cache = _get_supported_mime_types(
|
|
2149
|
-
_lib.c2pa_reader_supported_mime_types, cls._supported_mime_types_cache
|
|
2150
|
-
)
|
|
2172
|
+
_lib.c2pa_reader_supported_mime_types, cls._supported_mime_types_cache)
|
|
2151
2173
|
return result
|
|
2152
2174
|
|
|
2153
2175
|
@classmethod
|
|
@@ -2330,8 +2352,9 @@ class Reader(ManagedResource):
|
|
|
2330
2352
|
else:
|
|
2331
2353
|
if not isinstance(manifest_data, bytes):
|
|
2332
2354
|
raise TypeError(Reader._ERROR_MESSAGES['manifest_error'])
|
|
2333
|
-
manifest_array = (
|
|
2334
|
-
|
|
2355
|
+
manifest_array = (
|
|
2356
|
+
ctypes.c_ubyte *
|
|
2357
|
+
len(manifest_data)).from_buffer_copy(manifest_data)
|
|
2335
2358
|
self._handle = (
|
|
2336
2359
|
_lib.c2pa_reader_from_manifest_data_and_stream(
|
|
2337
2360
|
format_bytes,
|
|
@@ -2341,9 +2364,9 @@ class Reader(ManagedResource):
|
|
|
2341
2364
|
)
|
|
2342
2365
|
)
|
|
2343
2366
|
|
|
2344
|
-
_check_ffi_operation_result(
|
|
2345
|
-
|
|
2346
|
-
|
|
2367
|
+
_check_ffi_operation_result(
|
|
2368
|
+
self._handle,
|
|
2369
|
+
Reader._ERROR_MESSAGES['reader_error'].format("Unknown error"))
|
|
2347
2370
|
|
|
2348
2371
|
def _init_from_file(self, path, format_bytes,
|
|
2349
2372
|
manifest_data=None):
|
|
@@ -2403,19 +2426,25 @@ class Reader(ManagedResource):
|
|
|
2403
2426
|
reader_ptr = _lib.c2pa_reader_from_context(
|
|
2404
2427
|
context.execution_context,
|
|
2405
2428
|
)
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2429
|
+
try:
|
|
2430
|
+
_check_ffi_operation_result(reader_ptr,
|
|
2431
|
+
Reader._ERROR_MESSAGES[
|
|
2432
|
+
'reader_error'
|
|
2433
|
+
].format("Unknown error")
|
|
2434
|
+
)
|
|
2435
|
+
except Exception:
|
|
2436
|
+
if reader_ptr:
|
|
2437
|
+
ManagedResource._free_native_ptr(reader_ptr)
|
|
2438
|
+
raise
|
|
2411
2439
|
|
|
2412
2440
|
if manifest_data is not None:
|
|
2413
2441
|
if not isinstance(manifest_data, bytes):
|
|
2414
2442
|
raise TypeError(
|
|
2415
2443
|
Reader._ERROR_MESSAGES[
|
|
2416
2444
|
'manifest_error'])
|
|
2417
|
-
manifest_array = (
|
|
2418
|
-
|
|
2445
|
+
manifest_array = (
|
|
2446
|
+
ctypes.c_ubyte *
|
|
2447
|
+
len(manifest_data)).from_buffer_copy(manifest_data)
|
|
2419
2448
|
# Consume current reader,
|
|
2420
2449
|
# with manifest data and stream (C FFI pattern),
|
|
2421
2450
|
# to create a new one (switch out)
|
|
@@ -2440,10 +2469,10 @@ class Reader(ManagedResource):
|
|
|
2440
2469
|
self._handle = new_ptr
|
|
2441
2470
|
|
|
2442
2471
|
_check_ffi_operation_result(new_ptr,
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2472
|
+
Reader._ERROR_MESSAGES[
|
|
2473
|
+
'reader_error'
|
|
2474
|
+
].format("Unknown error")
|
|
2475
|
+
)
|
|
2447
2476
|
|
|
2448
2477
|
self._lifecycle_state = LifecycleState.ACTIVE
|
|
2449
2478
|
except Exception:
|
|
@@ -2534,9 +2563,9 @@ class Reader(ManagedResource):
|
|
|
2534
2563
|
if not new_ptr:
|
|
2535
2564
|
self._mark_consumed()
|
|
2536
2565
|
_check_ffi_operation_result(new_ptr,
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2566
|
+
Reader._ERROR_MESSAGES[
|
|
2567
|
+
'fragment_error'
|
|
2568
|
+
].format("Unknown error"))
|
|
2540
2569
|
self._handle = new_ptr
|
|
2541
2570
|
|
|
2542
2571
|
# Invalidate caches: processing a new BMFF fragment updates the native
|
|
@@ -2572,7 +2601,7 @@ class Reader(ManagedResource):
|
|
|
2572
2601
|
|
|
2573
2602
|
result = _lib.c2pa_reader_json(self._handle)
|
|
2574
2603
|
_check_ffi_operation_result(result,
|
|
2575
|
-
|
|
2604
|
+
"Error during manifest parsing in Reader")
|
|
2576
2605
|
|
|
2577
2606
|
# Cache the result and return it
|
|
2578
2607
|
self._manifest_json_str_cache = _convert_to_py_string(result)
|
|
@@ -2597,8 +2626,8 @@ class Reader(ManagedResource):
|
|
|
2597
2626
|
self._ensure_valid_state()
|
|
2598
2627
|
|
|
2599
2628
|
result = _lib.c2pa_reader_detailed_json(self._handle)
|
|
2600
|
-
_check_ffi_operation_result(
|
|
2601
|
-
"Error during detailed manifest parsing in Reader")
|
|
2629
|
+
_check_ffi_operation_result(
|
|
2630
|
+
result, "Error during detailed manifest parsing in Reader")
|
|
2602
2631
|
|
|
2603
2632
|
return _convert_to_py_string(result)
|
|
2604
2633
|
|
|
@@ -2723,7 +2752,8 @@ class Reader(ManagedResource):
|
|
|
2723
2752
|
result = _lib.c2pa_reader_resource_to_stream(
|
|
2724
2753
|
self._handle, uri_str, stream_obj._stream)
|
|
2725
2754
|
|
|
2726
|
-
_check_ffi_operation_result(
|
|
2755
|
+
_check_ffi_operation_result(
|
|
2756
|
+
result,
|
|
2727
2757
|
"Error during resource {} to stream conversion".format(uri),
|
|
2728
2758
|
check=lambda r: r < 0)
|
|
2729
2759
|
|
|
@@ -2772,7 +2802,6 @@ class Reader(ManagedResource):
|
|
|
2772
2802
|
class Signer(ManagedResource):
|
|
2773
2803
|
"""High-level wrapper for C2PA Signer operations."""
|
|
2774
2804
|
|
|
2775
|
-
|
|
2776
2805
|
# Class-level error messages to avoid multiple creation
|
|
2777
2806
|
_ERROR_MESSAGES = {
|
|
2778
2807
|
'closed_error': "Signer is closed",
|
|
@@ -2805,8 +2834,8 @@ class Signer(ManagedResource):
|
|
|
2805
2834
|
|
|
2806
2835
|
signer_ptr = _lib.c2pa_signer_from_info(ctypes.byref(signer_info))
|
|
2807
2836
|
|
|
2808
|
-
_check_ffi_operation_result(
|
|
2809
|
-
"Failed to create signer from configured signer_info")
|
|
2837
|
+
_check_ffi_operation_result(
|
|
2838
|
+
signer_ptr, "Failed to create signer from configured signer_info")
|
|
2810
2839
|
|
|
2811
2840
|
return cls(signer_ptr)
|
|
2812
2841
|
|
|
@@ -2937,7 +2966,7 @@ class Signer(ManagedResource):
|
|
|
2937
2966
|
)
|
|
2938
2967
|
|
|
2939
2968
|
_check_ffi_operation_result(signer_ptr,
|
|
2940
|
-
|
|
2969
|
+
"Failed to create signer")
|
|
2941
2970
|
|
|
2942
2971
|
# Create and return the signer instance with the callback
|
|
2943
2972
|
signer_instance = cls(signer_ptr)
|
|
@@ -2987,8 +3016,10 @@ class Signer(ManagedResource):
|
|
|
2987
3016
|
|
|
2988
3017
|
result = _lib.c2pa_signer_reserve_size(self._handle)
|
|
2989
3018
|
|
|
2990
|
-
_check_ffi_operation_result(
|
|
2991
|
-
|
|
3019
|
+
_check_ffi_operation_result(
|
|
3020
|
+
result,
|
|
3021
|
+
"Failed to get reserve size",
|
|
3022
|
+
check=lambda r: r < 0)
|
|
2992
3023
|
|
|
2993
3024
|
return result
|
|
2994
3025
|
|
|
@@ -2996,7 +3027,6 @@ class Signer(ManagedResource):
|
|
|
2996
3027
|
class Builder(ManagedResource):
|
|
2997
3028
|
"""High-level wrapper for C2PA Builder operations."""
|
|
2998
3029
|
|
|
2999
|
-
|
|
3000
3030
|
# Supported mimetypes cache
|
|
3001
3031
|
_supported_mime_types_cache = None
|
|
3002
3032
|
|
|
@@ -3029,8 +3059,7 @@ class Builder(ManagedResource):
|
|
|
3029
3059
|
C2paError: If there was an error retrieving the MIME types
|
|
3030
3060
|
"""
|
|
3031
3061
|
result, cls._supported_mime_types_cache = _get_supported_mime_types(
|
|
3032
|
-
_lib.c2pa_builder_supported_mime_types, cls._supported_mime_types_cache
|
|
3033
|
-
)
|
|
3062
|
+
_lib.c2pa_builder_supported_mime_types, cls._supported_mime_types_cache)
|
|
3034
3063
|
return result
|
|
3035
3064
|
|
|
3036
3065
|
@classmethod
|
|
@@ -3103,8 +3132,8 @@ class Builder(ManagedResource):
|
|
|
3103
3132
|
)
|
|
3104
3133
|
|
|
3105
3134
|
_check_ffi_operation_result(builder._handle,
|
|
3106
|
-
|
|
3107
|
-
|
|
3135
|
+
"Failed to create builder from archive"
|
|
3136
|
+
)
|
|
3108
3137
|
|
|
3109
3138
|
builder._lifecycle_state = LifecycleState.ACTIVE
|
|
3110
3139
|
return builder
|
|
@@ -3156,11 +3185,9 @@ class Builder(ManagedResource):
|
|
|
3156
3185
|
else:
|
|
3157
3186
|
self._handle = _lib.c2pa_builder_from_json(json_str)
|
|
3158
3187
|
|
|
3159
|
-
_check_ffi_operation_result(
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
)
|
|
3163
|
-
)
|
|
3188
|
+
_check_ffi_operation_result(
|
|
3189
|
+
self._handle,
|
|
3190
|
+
Builder._ERROR_MESSAGES['builder_error'].format("Unknown error"))
|
|
3164
3191
|
|
|
3165
3192
|
self._lifecycle_state = LifecycleState.ACTIVE
|
|
3166
3193
|
|
|
@@ -3176,11 +3203,16 @@ class Builder(ManagedResource):
|
|
|
3176
3203
|
builder_ptr = _lib.c2pa_builder_from_context(
|
|
3177
3204
|
context.execution_context,
|
|
3178
3205
|
)
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3206
|
+
try:
|
|
3207
|
+
_check_ffi_operation_result(builder_ptr,
|
|
3208
|
+
Builder._ERROR_MESSAGES[
|
|
3209
|
+
'builder_error'
|
|
3210
|
+
].format("Unknown error")
|
|
3211
|
+
)
|
|
3212
|
+
except Exception:
|
|
3213
|
+
if builder_ptr:
|
|
3214
|
+
ManagedResource._free_native_ptr(builder_ptr)
|
|
3215
|
+
raise
|
|
3184
3216
|
|
|
3185
3217
|
# Consume-and-return: builder_ptr is consumed,
|
|
3186
3218
|
# new_ptr is the valid pointer going forward
|
|
@@ -3188,10 +3220,10 @@ class Builder(ManagedResource):
|
|
|
3188
3220
|
self._handle = new_ptr
|
|
3189
3221
|
|
|
3190
3222
|
_check_ffi_operation_result(new_ptr,
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3223
|
+
Builder._ERROR_MESSAGES[
|
|
3224
|
+
'builder_error'
|
|
3225
|
+
].format("Unknown error")
|
|
3226
|
+
)
|
|
3195
3227
|
|
|
3196
3228
|
def set_no_embed(self):
|
|
3197
3229
|
"""Set the no-embed flag.
|
|
@@ -3220,7 +3252,8 @@ class Builder(ManagedResource):
|
|
|
3220
3252
|
url_bytes = _to_utf8_bytes(remote_url, "remote URL")
|
|
3221
3253
|
result = _lib.c2pa_builder_set_remote_url(self._handle, url_bytes)
|
|
3222
3254
|
|
|
3223
|
-
_check_ffi_operation_result(
|
|
3255
|
+
_check_ffi_operation_result(
|
|
3256
|
+
result,
|
|
3224
3257
|
Builder._ERROR_MESSAGES['url_error'].format("Unknown error"),
|
|
3225
3258
|
check=lambda r: r != 0)
|
|
3226
3259
|
|
|
@@ -3258,7 +3291,8 @@ class Builder(ManagedResource):
|
|
|
3258
3291
|
ctypes.c_uint(digital_source_type),
|
|
3259
3292
|
)
|
|
3260
3293
|
|
|
3261
|
-
_check_ffi_operation_result(
|
|
3294
|
+
_check_ffi_operation_result(
|
|
3295
|
+
result,
|
|
3262
3296
|
"Error setting intent for Builder: Unknown error",
|
|
3263
3297
|
check=lambda r: r != 0)
|
|
3264
3298
|
|
|
@@ -3280,10 +3314,9 @@ class Builder(ManagedResource):
|
|
|
3280
3314
|
result = _lib.c2pa_builder_add_resource(
|
|
3281
3315
|
self._handle, uri_bytes, stream_obj._stream)
|
|
3282
3316
|
|
|
3283
|
-
_check_ffi_operation_result(
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
),
|
|
3317
|
+
_check_ffi_operation_result(
|
|
3318
|
+
result,
|
|
3319
|
+
Builder._ERROR_MESSAGES['resource_error'].format("Unknown error"),
|
|
3287
3320
|
check=lambda r: r != 0)
|
|
3288
3321
|
|
|
3289
3322
|
def add_ingredient(
|
|
@@ -3348,10 +3381,9 @@ class Builder(ManagedResource):
|
|
|
3348
3381
|
)
|
|
3349
3382
|
)
|
|
3350
3383
|
|
|
3351
|
-
_check_ffi_operation_result(
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
),
|
|
3384
|
+
_check_ffi_operation_result(
|
|
3385
|
+
result,
|
|
3386
|
+
Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"),
|
|
3355
3387
|
check=lambda r: r != 0)
|
|
3356
3388
|
|
|
3357
3389
|
def add_ingredient_from_file_path(
|
|
@@ -3417,10 +3449,9 @@ class Builder(ManagedResource):
|
|
|
3417
3449
|
action_str = _to_utf8_bytes(action_json, "action JSON")
|
|
3418
3450
|
result = _lib.c2pa_builder_add_action(self._handle, action_str)
|
|
3419
3451
|
|
|
3420
|
-
_check_ffi_operation_result(
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
),
|
|
3452
|
+
_check_ffi_operation_result(
|
|
3453
|
+
result,
|
|
3454
|
+
Builder._ERROR_MESSAGES['action_error'].format("Unknown error"),
|
|
3424
3455
|
check=lambda r: r != 0)
|
|
3425
3456
|
|
|
3426
3457
|
def to_archive(self, stream: Any) -> None:
|
|
@@ -3439,10 +3470,9 @@ class Builder(ManagedResource):
|
|
|
3439
3470
|
result = _lib.c2pa_builder_to_archive(
|
|
3440
3471
|
self._handle, stream_obj._stream)
|
|
3441
3472
|
|
|
3442
|
-
_check_ffi_operation_result(
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
),
|
|
3473
|
+
_check_ffi_operation_result(
|
|
3474
|
+
result,
|
|
3475
|
+
Builder._ERROR_MESSAGES["archive_error"].format("Unknown error"),
|
|
3446
3476
|
check=lambda r: r != 0)
|
|
3447
3477
|
|
|
3448
3478
|
def with_archive(self, stream: Any) -> 'Builder':
|
|
@@ -3464,7 +3494,8 @@ class Builder(ManagedResource):
|
|
|
3464
3494
|
|
|
3465
3495
|
with Stream(stream) as stream_obj:
|
|
3466
3496
|
try:
|
|
3467
|
-
new_ptr = _lib.c2pa_builder_with_archive(
|
|
3497
|
+
new_ptr = _lib.c2pa_builder_with_archive(
|
|
3498
|
+
self._handle, stream_obj._stream)
|
|
3468
3499
|
except Exception as e:
|
|
3469
3500
|
self._mark_consumed()
|
|
3470
3501
|
raise C2paError(
|
|
@@ -3472,7 +3503,8 @@ class Builder(ManagedResource):
|
|
|
3472
3503
|
)
|
|
3473
3504
|
# Old handle consumed by FFI
|
|
3474
3505
|
self._handle = new_ptr
|
|
3475
|
-
_check_ffi_operation_result(
|
|
3506
|
+
_check_ffi_operation_result(
|
|
3507
|
+
new_ptr, "Failed to load archive into builder")
|
|
3476
3508
|
|
|
3477
3509
|
return self
|
|
3478
3510
|
|
|
@@ -3539,8 +3571,10 @@ class Builder(ManagedResource):
|
|
|
3539
3571
|
self.close()
|
|
3540
3572
|
raise C2paError(f"Error during signing: {e}")
|
|
3541
3573
|
|
|
3542
|
-
_check_ffi_operation_result(
|
|
3543
|
-
|
|
3574
|
+
_check_ffi_operation_result(
|
|
3575
|
+
result,
|
|
3576
|
+
"Error during signing",
|
|
3577
|
+
check=lambda r: r < 0)
|
|
3544
3578
|
|
|
3545
3579
|
# Capture the manifest bytes if available
|
|
3546
3580
|
manifest_bytes = b""
|
|
@@ -3596,7 +3630,8 @@ class Builder(ManagedResource):
|
|
|
3596
3630
|
signer=signer,
|
|
3597
3631
|
)
|
|
3598
3632
|
elif self._has_context_signer:
|
|
3599
|
-
manifest_bytes = self._sign_internal(
|
|
3633
|
+
manifest_bytes = self._sign_internal(
|
|
3634
|
+
format, source_stream, dest_stream)
|
|
3600
3635
|
else:
|
|
3601
3636
|
raise C2paError(
|
|
3602
3637
|
"No signer provided. Either pass a"
|
|
@@ -3765,8 +3800,10 @@ def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]:
|
|
|
3765
3800
|
ctypes.byref(result_bytes_ptr)
|
|
3766
3801
|
)
|
|
3767
3802
|
|
|
3768
|
-
_check_ffi_operation_result(
|
|
3769
|
-
|
|
3803
|
+
_check_ffi_operation_result(
|
|
3804
|
+
result,
|
|
3805
|
+
"Failed to format embeddable manifest",
|
|
3806
|
+
check=lambda r: r < 0)
|
|
3770
3807
|
|
|
3771
3808
|
size = result
|
|
3772
3809
|
try:
|
|
@@ -3897,7 +3934,7 @@ def ed25519_sign(data: bytes, private_key: str) -> bytes:
|
|
|
3897
3934
|
)
|
|
3898
3935
|
|
|
3899
3936
|
_check_ffi_operation_result(signature_ptr,
|
|
3900
|
-
|
|
3937
|
+
"Failed to sign data with Ed25519")
|
|
3901
3938
|
|
|
3902
3939
|
try:
|
|
3903
3940
|
# Ed25519 signatures are always 64 bytes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.32.
|
|
3
|
+
Version: 0.32.13
|
|
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>
|
|
@@ -69,6 +69,48 @@ To use the module in Python code, import the module like this:
|
|
|
69
69
|
import c2pa
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Building from local c2pa-rs sources
|
|
73
|
+
|
|
74
|
+
### Using a virtual environment with local builds
|
|
75
|
+
|
|
76
|
+
The `make` targets honor an active virtualenv. Create a virtual environment `./.venv` and activate it before running them so commands use the project interpreter rather than the global Python interpreter:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
make create-venv && source .venv/bin/activate
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Build steps
|
|
83
|
+
|
|
84
|
+
By default the build downloads a prebuilt native library from a [c2pa-rs](https://github.com/contentauth/c2pa-rs) release. To test the Python bindings against a local, unreleased c2pa-rs checkout, you can instead build the native library from source.
|
|
85
|
+
|
|
86
|
+
Prerequisites:
|
|
87
|
+
|
|
88
|
+
- A local clone of [c2pa-rs](https://github.com/contentauth/c2pa-rs).
|
|
89
|
+
- The [Rust toolchain](https://rust-lang.org/tools/install/) (`cargo` on your `PATH`).
|
|
90
|
+
|
|
91
|
+
Point `C2PA_RS_PATH` at your c2pa-rs checkout and run the `build-from-source` target:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
export C2PA_RS_PATH=/path/to/c2pa-rs
|
|
95
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This does a clean build of the `c2pa-c-ffi` crate (with the `file_io` feature, which the Python wrapper requires), stages the resulting library under both `artifacts/` and `src/c2pa/libs/`, and installs the package in editable mode, replacing any prebuilt artifacts from `make download-native-artifacts`. The release profile is used by default; to build the debug profile instead, pass `EXTRA_BUILD_ARGS="--debug"`:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
make build-from-source C2PA_RS_PATH=$C2PA_RS_PATH EXTRA_BUILD_ARGS="--debug"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Note on targets for macOS
|
|
105
|
+
|
|
106
|
+
On macOS this produces a universal (arm64+x86_64) library by default, which requires both Rust targets:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To build a single-architecture library instead, set `C2PA_LIBS_PLATFORM` to a specific platform (for example `aarch64-apple-darwin`).
|
|
113
|
+
|
|
72
114
|
## Examples
|
|
73
115
|
|
|
74
116
|
See the [`examples` directory](https://github.com/contentauth/c2pa-python/tree/main/examples) for some helpful examples:
|
|
@@ -67,10 +67,25 @@ def load_test_settings_json():
|
|
|
67
67
|
return settings_data
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def parse_native_version():
|
|
71
|
+
"""
|
|
72
|
+
Parse the expected native SDK version from c2pa-native-version.txt.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: The semantic version string (e.g. "0.85.2").
|
|
76
|
+
"""
|
|
77
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
78
|
+
version_path = os.path.join(repo_root, 'c2pa-native-version.txt')
|
|
79
|
+
with open(version_path, 'r') as f:
|
|
80
|
+
raw = f.read().strip()
|
|
81
|
+
# Strip the "c2pa-v" prefix to get the bare semantic version.
|
|
82
|
+
return raw.split('v', 1)[1] if 'v' in raw else raw
|
|
83
|
+
|
|
84
|
+
|
|
70
85
|
class TestC2paSdk(unittest.TestCase):
|
|
71
86
|
def test_sdk_version(self):
|
|
72
87
|
# This test verifies the native libraries used match the expected version.
|
|
73
|
-
self.assertIn(
|
|
88
|
+
self.assertIn(parse_native_version(), sdk_version())
|
|
74
89
|
|
|
75
90
|
|
|
76
91
|
class TestReader(unittest.TestCase):
|
|
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
|
|
File without changes
|
|
File without changes
|