c2pa-python 0.32.12__tar.gz → 0.32.14__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.12/src/c2pa_python.egg-info → c2pa_python-0.32.14}/PKG-INFO +43 -1
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/README.md +42 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/pyproject.toml +1 -1
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/c2pa.py +122 -87
- {c2pa_python-0.32.12 → c2pa_python-0.32.14/src/c2pa_python.egg-info}/PKG-INFO +43 -1
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/tests/test_unit_tests.py +29 -1
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/LICENSE-APACHE +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/LICENSE-MIT +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/MANIFEST.in +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/requirements.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/setup.cfg +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/setup.py +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/build.py +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/lib.py +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.32.12 → c2pa_python-0.32.14}/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.14
|
|
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.14"
|
|
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" }
|
|
@@ -48,6 +48,7 @@ _REQUIRED_FUNCTIONS = [
|
|
|
48
48
|
'c2pa_reader_from_manifest_data_and_stream',
|
|
49
49
|
'c2pa_reader_json',
|
|
50
50
|
'c2pa_reader_detailed_json',
|
|
51
|
+
'c2pa_reader_crjson',
|
|
51
52
|
'c2pa_reader_resource_to_stream',
|
|
52
53
|
'c2pa_reader_from_context',
|
|
53
54
|
'c2pa_reader_with_stream',
|
|
@@ -253,7 +254,8 @@ class ManagedResource:
|
|
|
253
254
|
if self._lifecycle_state != LifecycleState.ACTIVE:
|
|
254
255
|
raise C2paError(f"{name} is not properly initialized")
|
|
255
256
|
if not self._handle:
|
|
256
|
-
raise C2paError(
|
|
257
|
+
raise C2paError(
|
|
258
|
+
f"{name} has an invalid internal state (active but no handle)")
|
|
257
259
|
_clear_error_state()
|
|
258
260
|
|
|
259
261
|
def _release(self):
|
|
@@ -560,6 +562,9 @@ _setup_function(
|
|
|
560
562
|
_setup_function(
|
|
561
563
|
_lib.c2pa_reader_detailed_json, [
|
|
562
564
|
ctypes.POINTER(C2paReader)], ctypes.c_void_p)
|
|
565
|
+
_setup_function(
|
|
566
|
+
_lib.c2pa_reader_crjson, [
|
|
567
|
+
ctypes.POINTER(C2paReader)], ctypes.c_void_p)
|
|
563
568
|
_setup_function(_lib.c2pa_reader_resource_to_stream, [ctypes.POINTER(
|
|
564
569
|
C2paReader), ctypes.c_char_p, ctypes.POINTER(C2paStream)], ctypes.c_int64)
|
|
565
570
|
_setup_function(
|
|
@@ -1011,7 +1016,11 @@ def _parse_operation_result_for_error(
|
|
|
1011
1016
|
return None
|
|
1012
1017
|
|
|
1013
1018
|
|
|
1014
|
-
def _check_ffi_operation_result(
|
|
1019
|
+
def _check_ffi_operation_result(
|
|
1020
|
+
result,
|
|
1021
|
+
fallback_msg,
|
|
1022
|
+
*,
|
|
1023
|
+
check=lambda r: not r):
|
|
1015
1024
|
"""Check an FFI native call result and raise C2paError if it indicates failure.
|
|
1016
1025
|
|
|
1017
1026
|
Args:
|
|
@@ -1036,7 +1045,8 @@ def _check_ffi_operation_result(result, fallback_msg, *, check=lambda r: not r):
|
|
|
1036
1045
|
return result
|
|
1037
1046
|
|
|
1038
1047
|
|
|
1039
|
-
def _to_utf8_bytes(data: Union[str, dict],
|
|
1048
|
+
def _to_utf8_bytes(data: Union[str, dict],
|
|
1049
|
+
error_context: str = "input") -> bytes:
|
|
1040
1050
|
"""Convert a string or dict to UTF-8 bytes.
|
|
1041
1051
|
|
|
1042
1052
|
If data is a dict, it is serialized to JSON first.
|
|
@@ -1059,8 +1069,8 @@ def _to_utf8_bytes(data: Union[str, dict], error_context: str = "input") -> byte
|
|
|
1059
1069
|
raise C2paError.Json(f"Failed to serialize {error_context}: {e}")
|
|
1060
1070
|
if not isinstance(data, str):
|
|
1061
1071
|
raise C2paError.Encoding(
|
|
1062
|
-
f"Expected str or dict for {error_context},
|
|
1063
|
-
|
|
1072
|
+
f"Expected str or dict for {error_context}, "
|
|
1073
|
+
f"got {type(data).__name__}")
|
|
1064
1074
|
try:
|
|
1065
1075
|
return data.encode('utf-8')
|
|
1066
1076
|
except UnicodeError as e:
|
|
@@ -1146,7 +1156,10 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None:
|
|
|
1146
1156
|
raise C2paError(f"Failed to encode settings to UTF-8: {e}")
|
|
1147
1157
|
|
|
1148
1158
|
result = _lib.c2pa_load_settings(settings_bytes, format_bytes)
|
|
1149
|
-
_check_ffi_operation_result(
|
|
1159
|
+
_check_ffi_operation_result(
|
|
1160
|
+
result,
|
|
1161
|
+
"Error loading settings",
|
|
1162
|
+
check=lambda r: r != 0)
|
|
1150
1163
|
|
|
1151
1164
|
return result
|
|
1152
1165
|
|
|
@@ -1219,7 +1232,8 @@ def read_ingredient_file(
|
|
|
1219
1232
|
result = _lib.c2pa_read_ingredient_file(
|
|
1220
1233
|
container._path_str, container._data_dir_str)
|
|
1221
1234
|
|
|
1222
|
-
_check_ffi_operation_result(
|
|
1235
|
+
_check_ffi_operation_result(
|
|
1236
|
+
result, "Error reading ingredient file {}".format(path))
|
|
1223
1237
|
|
|
1224
1238
|
return _convert_to_py_string(result)
|
|
1225
1239
|
|
|
@@ -1258,7 +1272,8 @@ def read_file(path: Union[str, Path],
|
|
|
1258
1272
|
container._data_dir_str = str(data_dir).encode('utf-8')
|
|
1259
1273
|
|
|
1260
1274
|
result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
|
|
1261
|
-
_check_ffi_operation_result(
|
|
1275
|
+
_check_ffi_operation_result(
|
|
1276
|
+
result, "Error during read of manifest from file {}".format(path))
|
|
1262
1277
|
|
|
1263
1278
|
return _convert_to_py_string(result)
|
|
1264
1279
|
|
|
@@ -1419,7 +1434,6 @@ class Settings(ManagedResource):
|
|
|
1419
1434
|
apply settings to Reader/Builder operations.
|
|
1420
1435
|
"""
|
|
1421
1436
|
|
|
1422
|
-
|
|
1423
1437
|
def __init__(self):
|
|
1424
1438
|
"""Create new Settings with default values."""
|
|
1425
1439
|
super().__init__()
|
|
@@ -1516,7 +1530,6 @@ class Settings(ManagedResource):
|
|
|
1516
1530
|
return self._handle
|
|
1517
1531
|
|
|
1518
1532
|
|
|
1519
|
-
|
|
1520
1533
|
class ContextBuilder:
|
|
1521
1534
|
"""Fluent builder for Context.
|
|
1522
1535
|
|
|
@@ -1574,7 +1587,6 @@ class Context(ManagedResource, ContextProvider):
|
|
|
1574
1587
|
used directly again after that.
|
|
1575
1588
|
"""
|
|
1576
1589
|
|
|
1577
|
-
|
|
1578
1590
|
def __init__(
|
|
1579
1591
|
self,
|
|
1580
1592
|
settings: Optional['Settings'] = None,
|
|
@@ -1718,7 +1730,6 @@ class Context(ManagedResource, ContextProvider):
|
|
|
1718
1730
|
return self._handle
|
|
1719
1731
|
|
|
1720
1732
|
|
|
1721
|
-
|
|
1722
1733
|
class Stream:
|
|
1723
1734
|
# Class-level somewhat atomic counter for generating
|
|
1724
1735
|
# unique stream IDs (useful for tracing streams usage in debug)
|
|
@@ -2132,7 +2143,6 @@ class Reader(ManagedResource):
|
|
|
2132
2143
|
Where `output` is either an in-memory stream or an opened file.
|
|
2133
2144
|
"""
|
|
2134
2145
|
|
|
2135
|
-
|
|
2136
2146
|
# Supported mimetypes cache
|
|
2137
2147
|
_supported_mime_types_cache = None
|
|
2138
2148
|
|
|
@@ -2163,8 +2173,7 @@ class Reader(ManagedResource):
|
|
|
2163
2173
|
C2paError: If there was an error retrieving the MIME types
|
|
2164
2174
|
"""
|
|
2165
2175
|
result, cls._supported_mime_types_cache = _get_supported_mime_types(
|
|
2166
|
-
_lib.c2pa_reader_supported_mime_types, cls._supported_mime_types_cache
|
|
2167
|
-
)
|
|
2176
|
+
_lib.c2pa_reader_supported_mime_types, cls._supported_mime_types_cache)
|
|
2168
2177
|
return result
|
|
2169
2178
|
|
|
2170
2179
|
@classmethod
|
|
@@ -2347,8 +2356,9 @@ class Reader(ManagedResource):
|
|
|
2347
2356
|
else:
|
|
2348
2357
|
if not isinstance(manifest_data, bytes):
|
|
2349
2358
|
raise TypeError(Reader._ERROR_MESSAGES['manifest_error'])
|
|
2350
|
-
manifest_array = (
|
|
2351
|
-
|
|
2359
|
+
manifest_array = (
|
|
2360
|
+
ctypes.c_ubyte *
|
|
2361
|
+
len(manifest_data)).from_buffer_copy(manifest_data)
|
|
2352
2362
|
self._handle = (
|
|
2353
2363
|
_lib.c2pa_reader_from_manifest_data_and_stream(
|
|
2354
2364
|
format_bytes,
|
|
@@ -2358,9 +2368,9 @@ class Reader(ManagedResource):
|
|
|
2358
2368
|
)
|
|
2359
2369
|
)
|
|
2360
2370
|
|
|
2361
|
-
_check_ffi_operation_result(
|
|
2362
|
-
|
|
2363
|
-
|
|
2371
|
+
_check_ffi_operation_result(
|
|
2372
|
+
self._handle,
|
|
2373
|
+
Reader._ERROR_MESSAGES['reader_error'].format("Unknown error"))
|
|
2364
2374
|
|
|
2365
2375
|
def _init_from_file(self, path, format_bytes,
|
|
2366
2376
|
manifest_data=None):
|
|
@@ -2422,10 +2432,10 @@ class Reader(ManagedResource):
|
|
|
2422
2432
|
)
|
|
2423
2433
|
try:
|
|
2424
2434
|
_check_ffi_operation_result(reader_ptr,
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2435
|
+
Reader._ERROR_MESSAGES[
|
|
2436
|
+
'reader_error'
|
|
2437
|
+
].format("Unknown error")
|
|
2438
|
+
)
|
|
2429
2439
|
except Exception:
|
|
2430
2440
|
if reader_ptr:
|
|
2431
2441
|
ManagedResource._free_native_ptr(reader_ptr)
|
|
@@ -2436,8 +2446,9 @@ class Reader(ManagedResource):
|
|
|
2436
2446
|
raise TypeError(
|
|
2437
2447
|
Reader._ERROR_MESSAGES[
|
|
2438
2448
|
'manifest_error'])
|
|
2439
|
-
manifest_array = (
|
|
2440
|
-
|
|
2449
|
+
manifest_array = (
|
|
2450
|
+
ctypes.c_ubyte *
|
|
2451
|
+
len(manifest_data)).from_buffer_copy(manifest_data)
|
|
2441
2452
|
# Consume current reader,
|
|
2442
2453
|
# with manifest data and stream (C FFI pattern),
|
|
2443
2454
|
# to create a new one (switch out)
|
|
@@ -2462,10 +2473,10 @@ class Reader(ManagedResource):
|
|
|
2462
2473
|
self._handle = new_ptr
|
|
2463
2474
|
|
|
2464
2475
|
_check_ffi_operation_result(new_ptr,
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2476
|
+
Reader._ERROR_MESSAGES[
|
|
2477
|
+
'reader_error'
|
|
2478
|
+
].format("Unknown error")
|
|
2479
|
+
)
|
|
2469
2480
|
|
|
2470
2481
|
self._lifecycle_state = LifecycleState.ACTIVE
|
|
2471
2482
|
except Exception:
|
|
@@ -2556,9 +2567,9 @@ class Reader(ManagedResource):
|
|
|
2556
2567
|
if not new_ptr:
|
|
2557
2568
|
self._mark_consumed()
|
|
2558
2569
|
_check_ffi_operation_result(new_ptr,
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2570
|
+
Reader._ERROR_MESSAGES[
|
|
2571
|
+
'fragment_error'
|
|
2572
|
+
].format("Unknown error"))
|
|
2562
2573
|
self._handle = new_ptr
|
|
2563
2574
|
|
|
2564
2575
|
# Invalidate caches: processing a new BMFF fragment updates the native
|
|
@@ -2594,7 +2605,7 @@ class Reader(ManagedResource):
|
|
|
2594
2605
|
|
|
2595
2606
|
result = _lib.c2pa_reader_json(self._handle)
|
|
2596
2607
|
_check_ffi_operation_result(result,
|
|
2597
|
-
|
|
2608
|
+
"Error during manifest parsing in Reader")
|
|
2598
2609
|
|
|
2599
2610
|
# Cache the result and return it
|
|
2600
2611
|
self._manifest_json_str_cache = _convert_to_py_string(result)
|
|
@@ -2619,8 +2630,29 @@ class Reader(ManagedResource):
|
|
|
2619
2630
|
self._ensure_valid_state()
|
|
2620
2631
|
|
|
2621
2632
|
result = _lib.c2pa_reader_detailed_json(self._handle)
|
|
2622
|
-
_check_ffi_operation_result(
|
|
2623
|
-
"Error during detailed manifest parsing in Reader")
|
|
2633
|
+
_check_ffi_operation_result(
|
|
2634
|
+
result, "Error during detailed manifest parsing in Reader")
|
|
2635
|
+
|
|
2636
|
+
return _convert_to_py_string(result)
|
|
2637
|
+
|
|
2638
|
+
def crjson(self) -> str:
|
|
2639
|
+
"""Get the manifest store as a crJSON string.
|
|
2640
|
+
|
|
2641
|
+
crJSON is a standardized JSON format for C2PA manifest data. This
|
|
2642
|
+
call yields empty JSON ("{}") when there are no Content Credentials.
|
|
2643
|
+
|
|
2644
|
+
Returns:
|
|
2645
|
+
The manifest store as a crJSON string.
|
|
2646
|
+
|
|
2647
|
+
Raises:
|
|
2648
|
+
C2paError: If the Reader has been closed or the underlying C
|
|
2649
|
+
call returns null.
|
|
2650
|
+
"""
|
|
2651
|
+
|
|
2652
|
+
self._ensure_valid_state()
|
|
2653
|
+
|
|
2654
|
+
result = _lib.c2pa_reader_crjson(self._handle)
|
|
2655
|
+
_check_ffi_operation_result(result, "Error parsing crJSON")
|
|
2624
2656
|
|
|
2625
2657
|
return _convert_to_py_string(result)
|
|
2626
2658
|
|
|
@@ -2745,7 +2777,8 @@ class Reader(ManagedResource):
|
|
|
2745
2777
|
result = _lib.c2pa_reader_resource_to_stream(
|
|
2746
2778
|
self._handle, uri_str, stream_obj._stream)
|
|
2747
2779
|
|
|
2748
|
-
_check_ffi_operation_result(
|
|
2780
|
+
_check_ffi_operation_result(
|
|
2781
|
+
result,
|
|
2749
2782
|
"Error during resource {} to stream conversion".format(uri),
|
|
2750
2783
|
check=lambda r: r < 0)
|
|
2751
2784
|
|
|
@@ -2794,7 +2827,6 @@ class Reader(ManagedResource):
|
|
|
2794
2827
|
class Signer(ManagedResource):
|
|
2795
2828
|
"""High-level wrapper for C2PA Signer operations."""
|
|
2796
2829
|
|
|
2797
|
-
|
|
2798
2830
|
# Class-level error messages to avoid multiple creation
|
|
2799
2831
|
_ERROR_MESSAGES = {
|
|
2800
2832
|
'closed_error': "Signer is closed",
|
|
@@ -2827,8 +2859,8 @@ class Signer(ManagedResource):
|
|
|
2827
2859
|
|
|
2828
2860
|
signer_ptr = _lib.c2pa_signer_from_info(ctypes.byref(signer_info))
|
|
2829
2861
|
|
|
2830
|
-
_check_ffi_operation_result(
|
|
2831
|
-
"Failed to create signer from configured signer_info")
|
|
2862
|
+
_check_ffi_operation_result(
|
|
2863
|
+
signer_ptr, "Failed to create signer from configured signer_info")
|
|
2832
2864
|
|
|
2833
2865
|
return cls(signer_ptr)
|
|
2834
2866
|
|
|
@@ -2959,7 +2991,7 @@ class Signer(ManagedResource):
|
|
|
2959
2991
|
)
|
|
2960
2992
|
|
|
2961
2993
|
_check_ffi_operation_result(signer_ptr,
|
|
2962
|
-
|
|
2994
|
+
"Failed to create signer")
|
|
2963
2995
|
|
|
2964
2996
|
# Create and return the signer instance with the callback
|
|
2965
2997
|
signer_instance = cls(signer_ptr)
|
|
@@ -3009,8 +3041,10 @@ class Signer(ManagedResource):
|
|
|
3009
3041
|
|
|
3010
3042
|
result = _lib.c2pa_signer_reserve_size(self._handle)
|
|
3011
3043
|
|
|
3012
|
-
_check_ffi_operation_result(
|
|
3013
|
-
|
|
3044
|
+
_check_ffi_operation_result(
|
|
3045
|
+
result,
|
|
3046
|
+
"Failed to get reserve size",
|
|
3047
|
+
check=lambda r: r < 0)
|
|
3014
3048
|
|
|
3015
3049
|
return result
|
|
3016
3050
|
|
|
@@ -3018,7 +3052,6 @@ class Signer(ManagedResource):
|
|
|
3018
3052
|
class Builder(ManagedResource):
|
|
3019
3053
|
"""High-level wrapper for C2PA Builder operations."""
|
|
3020
3054
|
|
|
3021
|
-
|
|
3022
3055
|
# Supported mimetypes cache
|
|
3023
3056
|
_supported_mime_types_cache = None
|
|
3024
3057
|
|
|
@@ -3051,8 +3084,7 @@ class Builder(ManagedResource):
|
|
|
3051
3084
|
C2paError: If there was an error retrieving the MIME types
|
|
3052
3085
|
"""
|
|
3053
3086
|
result, cls._supported_mime_types_cache = _get_supported_mime_types(
|
|
3054
|
-
_lib.c2pa_builder_supported_mime_types, cls._supported_mime_types_cache
|
|
3055
|
-
)
|
|
3087
|
+
_lib.c2pa_builder_supported_mime_types, cls._supported_mime_types_cache)
|
|
3056
3088
|
return result
|
|
3057
3089
|
|
|
3058
3090
|
@classmethod
|
|
@@ -3125,8 +3157,8 @@ class Builder(ManagedResource):
|
|
|
3125
3157
|
)
|
|
3126
3158
|
|
|
3127
3159
|
_check_ffi_operation_result(builder._handle,
|
|
3128
|
-
|
|
3129
|
-
|
|
3160
|
+
"Failed to create builder from archive"
|
|
3161
|
+
)
|
|
3130
3162
|
|
|
3131
3163
|
builder._lifecycle_state = LifecycleState.ACTIVE
|
|
3132
3164
|
return builder
|
|
@@ -3178,11 +3210,9 @@ class Builder(ManagedResource):
|
|
|
3178
3210
|
else:
|
|
3179
3211
|
self._handle = _lib.c2pa_builder_from_json(json_str)
|
|
3180
3212
|
|
|
3181
|
-
_check_ffi_operation_result(
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
)
|
|
3185
|
-
)
|
|
3213
|
+
_check_ffi_operation_result(
|
|
3214
|
+
self._handle,
|
|
3215
|
+
Builder._ERROR_MESSAGES['builder_error'].format("Unknown error"))
|
|
3186
3216
|
|
|
3187
3217
|
self._lifecycle_state = LifecycleState.ACTIVE
|
|
3188
3218
|
|
|
@@ -3200,10 +3230,10 @@ class Builder(ManagedResource):
|
|
|
3200
3230
|
)
|
|
3201
3231
|
try:
|
|
3202
3232
|
_check_ffi_operation_result(builder_ptr,
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3233
|
+
Builder._ERROR_MESSAGES[
|
|
3234
|
+
'builder_error'
|
|
3235
|
+
].format("Unknown error")
|
|
3236
|
+
)
|
|
3207
3237
|
except Exception:
|
|
3208
3238
|
if builder_ptr:
|
|
3209
3239
|
ManagedResource._free_native_ptr(builder_ptr)
|
|
@@ -3215,10 +3245,10 @@ class Builder(ManagedResource):
|
|
|
3215
3245
|
self._handle = new_ptr
|
|
3216
3246
|
|
|
3217
3247
|
_check_ffi_operation_result(new_ptr,
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3248
|
+
Builder._ERROR_MESSAGES[
|
|
3249
|
+
'builder_error'
|
|
3250
|
+
].format("Unknown error")
|
|
3251
|
+
)
|
|
3222
3252
|
|
|
3223
3253
|
def set_no_embed(self):
|
|
3224
3254
|
"""Set the no-embed flag.
|
|
@@ -3247,7 +3277,8 @@ class Builder(ManagedResource):
|
|
|
3247
3277
|
url_bytes = _to_utf8_bytes(remote_url, "remote URL")
|
|
3248
3278
|
result = _lib.c2pa_builder_set_remote_url(self._handle, url_bytes)
|
|
3249
3279
|
|
|
3250
|
-
_check_ffi_operation_result(
|
|
3280
|
+
_check_ffi_operation_result(
|
|
3281
|
+
result,
|
|
3251
3282
|
Builder._ERROR_MESSAGES['url_error'].format("Unknown error"),
|
|
3252
3283
|
check=lambda r: r != 0)
|
|
3253
3284
|
|
|
@@ -3285,7 +3316,8 @@ class Builder(ManagedResource):
|
|
|
3285
3316
|
ctypes.c_uint(digital_source_type),
|
|
3286
3317
|
)
|
|
3287
3318
|
|
|
3288
|
-
_check_ffi_operation_result(
|
|
3319
|
+
_check_ffi_operation_result(
|
|
3320
|
+
result,
|
|
3289
3321
|
"Error setting intent for Builder: Unknown error",
|
|
3290
3322
|
check=lambda r: r != 0)
|
|
3291
3323
|
|
|
@@ -3307,10 +3339,9 @@ class Builder(ManagedResource):
|
|
|
3307
3339
|
result = _lib.c2pa_builder_add_resource(
|
|
3308
3340
|
self._handle, uri_bytes, stream_obj._stream)
|
|
3309
3341
|
|
|
3310
|
-
_check_ffi_operation_result(
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
),
|
|
3342
|
+
_check_ffi_operation_result(
|
|
3343
|
+
result,
|
|
3344
|
+
Builder._ERROR_MESSAGES['resource_error'].format("Unknown error"),
|
|
3314
3345
|
check=lambda r: r != 0)
|
|
3315
3346
|
|
|
3316
3347
|
def add_ingredient(
|
|
@@ -3375,10 +3406,9 @@ class Builder(ManagedResource):
|
|
|
3375
3406
|
)
|
|
3376
3407
|
)
|
|
3377
3408
|
|
|
3378
|
-
_check_ffi_operation_result(
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
),
|
|
3409
|
+
_check_ffi_operation_result(
|
|
3410
|
+
result,
|
|
3411
|
+
Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"),
|
|
3382
3412
|
check=lambda r: r != 0)
|
|
3383
3413
|
|
|
3384
3414
|
def add_ingredient_from_file_path(
|
|
@@ -3444,10 +3474,9 @@ class Builder(ManagedResource):
|
|
|
3444
3474
|
action_str = _to_utf8_bytes(action_json, "action JSON")
|
|
3445
3475
|
result = _lib.c2pa_builder_add_action(self._handle, action_str)
|
|
3446
3476
|
|
|
3447
|
-
_check_ffi_operation_result(
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
),
|
|
3477
|
+
_check_ffi_operation_result(
|
|
3478
|
+
result,
|
|
3479
|
+
Builder._ERROR_MESSAGES['action_error'].format("Unknown error"),
|
|
3451
3480
|
check=lambda r: r != 0)
|
|
3452
3481
|
|
|
3453
3482
|
def to_archive(self, stream: Any) -> None:
|
|
@@ -3466,10 +3495,9 @@ class Builder(ManagedResource):
|
|
|
3466
3495
|
result = _lib.c2pa_builder_to_archive(
|
|
3467
3496
|
self._handle, stream_obj._stream)
|
|
3468
3497
|
|
|
3469
|
-
_check_ffi_operation_result(
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
),
|
|
3498
|
+
_check_ffi_operation_result(
|
|
3499
|
+
result,
|
|
3500
|
+
Builder._ERROR_MESSAGES["archive_error"].format("Unknown error"),
|
|
3473
3501
|
check=lambda r: r != 0)
|
|
3474
3502
|
|
|
3475
3503
|
def with_archive(self, stream: Any) -> 'Builder':
|
|
@@ -3491,7 +3519,8 @@ class Builder(ManagedResource):
|
|
|
3491
3519
|
|
|
3492
3520
|
with Stream(stream) as stream_obj:
|
|
3493
3521
|
try:
|
|
3494
|
-
new_ptr = _lib.c2pa_builder_with_archive(
|
|
3522
|
+
new_ptr = _lib.c2pa_builder_with_archive(
|
|
3523
|
+
self._handle, stream_obj._stream)
|
|
3495
3524
|
except Exception as e:
|
|
3496
3525
|
self._mark_consumed()
|
|
3497
3526
|
raise C2paError(
|
|
@@ -3499,7 +3528,8 @@ class Builder(ManagedResource):
|
|
|
3499
3528
|
)
|
|
3500
3529
|
# Old handle consumed by FFI
|
|
3501
3530
|
self._handle = new_ptr
|
|
3502
|
-
_check_ffi_operation_result(
|
|
3531
|
+
_check_ffi_operation_result(
|
|
3532
|
+
new_ptr, "Failed to load archive into builder")
|
|
3503
3533
|
|
|
3504
3534
|
return self
|
|
3505
3535
|
|
|
@@ -3566,8 +3596,10 @@ class Builder(ManagedResource):
|
|
|
3566
3596
|
self.close()
|
|
3567
3597
|
raise C2paError(f"Error during signing: {e}")
|
|
3568
3598
|
|
|
3569
|
-
_check_ffi_operation_result(
|
|
3570
|
-
|
|
3599
|
+
_check_ffi_operation_result(
|
|
3600
|
+
result,
|
|
3601
|
+
"Error during signing",
|
|
3602
|
+
check=lambda r: r < 0)
|
|
3571
3603
|
|
|
3572
3604
|
# Capture the manifest bytes if available
|
|
3573
3605
|
manifest_bytes = b""
|
|
@@ -3623,7 +3655,8 @@ class Builder(ManagedResource):
|
|
|
3623
3655
|
signer=signer,
|
|
3624
3656
|
)
|
|
3625
3657
|
elif self._has_context_signer:
|
|
3626
|
-
manifest_bytes = self._sign_internal(
|
|
3658
|
+
manifest_bytes = self._sign_internal(
|
|
3659
|
+
format, source_stream, dest_stream)
|
|
3627
3660
|
else:
|
|
3628
3661
|
raise C2paError(
|
|
3629
3662
|
"No signer provided. Either pass a"
|
|
@@ -3792,8 +3825,10 @@ def format_embeddable(format: str, manifest_bytes: bytes) -> tuple[int, bytes]:
|
|
|
3792
3825
|
ctypes.byref(result_bytes_ptr)
|
|
3793
3826
|
)
|
|
3794
3827
|
|
|
3795
|
-
_check_ffi_operation_result(
|
|
3796
|
-
|
|
3828
|
+
_check_ffi_operation_result(
|
|
3829
|
+
result,
|
|
3830
|
+
"Failed to format embeddable manifest",
|
|
3831
|
+
check=lambda r: r < 0)
|
|
3797
3832
|
|
|
3798
3833
|
size = result
|
|
3799
3834
|
try:
|
|
@@ -3924,7 +3959,7 @@ def ed25519_sign(data: bytes, private_key: str) -> bytes:
|
|
|
3924
3959
|
)
|
|
3925
3960
|
|
|
3926
3961
|
_check_ffi_operation_result(signature_ptr,
|
|
3927
|
-
|
|
3962
|
+
"Failed to sign data with Ed25519")
|
|
3928
3963
|
|
|
3929
3964
|
try:
|
|
3930
3965
|
# 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.14
|
|
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):
|
|
@@ -256,6 +271,19 @@ class TestReader(unittest.TestCase):
|
|
|
256
271
|
title = manifest_store["manifests"][manifest_store["active_manifest"]]["claim"]["dc:title"]
|
|
257
272
|
self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
|
|
258
273
|
|
|
274
|
+
def test_stream_read_crjson_and_parse(self):
|
|
275
|
+
with open(self.testPath, "rb") as file:
|
|
276
|
+
reader = Reader("image/jpeg", file)
|
|
277
|
+
crjson = reader.crjson()
|
|
278
|
+
self.assertTrue(crjson)
|
|
279
|
+
json.loads(crjson)
|
|
280
|
+
|
|
281
|
+
def test_stream_read_crjson_path_only(self):
|
|
282
|
+
with Reader(self.testPath) as reader:
|
|
283
|
+
crjson = reader.crjson()
|
|
284
|
+
self.assertTrue(crjson)
|
|
285
|
+
json.loads(crjson)
|
|
286
|
+
|
|
259
287
|
def test_stream_read_string_stream_path_only(self):
|
|
260
288
|
with Reader(self.testPath) as reader:
|
|
261
289
|
json_data = reader.json()
|
|
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
|