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.
Files changed (22) hide show
  1. {c2pa_python-0.32.11/src/c2pa_python.egg-info → c2pa_python-0.32.13}/PKG-INFO +43 -1
  2. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/README.md +42 -0
  3. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/pyproject.toml +1 -1
  4. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/c2pa.py +130 -93
  5. {c2pa_python-0.32.11 → c2pa_python-0.32.13/src/c2pa_python.egg-info}/PKG-INFO +43 -1
  6. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/tests/test_unit_tests.py +16 -1
  7. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/LICENSE-APACHE +0 -0
  8. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/LICENSE-MIT +0 -0
  9. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/MANIFEST.in +0 -0
  10. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/requirements.txt +0 -0
  11. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/scripts/download_artifacts.py +0 -0
  12. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/setup.cfg +0 -0
  13. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/setup.py +0 -0
  14. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/__init__.py +0 -0
  15. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/build.py +0 -0
  16. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.32.11 → c2pa_python-0.32.13}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {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.11
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.11"
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(f"{name} has an invalid internal state (active but no handle)")
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(result, fallback_msg, *, check=lambda r: not r):
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], error_context: str = "input") -> bytes:
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}, got {type(data).__name__}"
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(result, "Error loading settings", check=lambda r: r != 0)
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(result, "Error reading ingredient file {}".format(path))
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(result, "Error during read of manifest from file {}".format(path))
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
- _check_ffi_operation_result(ptr, "Failed to create Settings")
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
- buffer = self._file_like_stream.read(length)
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
- file_stream.seek(offset, whence)
1845
- return file_stream.tell()
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 = (ctypes.c_ubyte * len(manifest_data)).from_buffer_copy(
2334
- manifest_data)
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(self._handle,
2345
- Reader._ERROR_MESSAGES['reader_error'].format("Unknown error")
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
- _check_ffi_operation_result(reader_ptr,
2407
- Reader._ERROR_MESSAGES[
2408
- 'reader_error'
2409
- ].format("Unknown error")
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 = (ctypes.c_ubyte * len(manifest_data)).from_buffer_copy(
2418
- manifest_data)
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
- Reader._ERROR_MESSAGES[
2444
- 'reader_error'
2445
- ].format("Unknown error")
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
- Reader._ERROR_MESSAGES[
2538
- 'fragment_error'
2539
- ].format("Unknown error"))
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
- "Error during manifest parsing in Reader")
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(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(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(signer_ptr,
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
- "Failed to create signer")
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(result,
2991
- "Failed to get reserve size", check=lambda r: r < 0)
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
- "Failed to create builder from archive"
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(self._handle,
3160
- Builder._ERROR_MESSAGES['builder_error'].format(
3161
- "Unknown error"
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
- _check_ffi_operation_result(builder_ptr,
3180
- Builder._ERROR_MESSAGES[
3181
- 'builder_error'
3182
- ].format("Unknown error")
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
- Builder._ERROR_MESSAGES[
3192
- 'builder_error'
3193
- ].format("Unknown error")
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(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(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(result,
3284
- Builder._ERROR_MESSAGES['resource_error'].format(
3285
- "Unknown error"
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(result,
3352
- Builder._ERROR_MESSAGES['ingredient_error'].format(
3353
- "Unknown error"
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(result,
3421
- Builder._ERROR_MESSAGES['action_error'].format(
3422
- "Unknown error"
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(result,
3443
- Builder._ERROR_MESSAGES["archive_error"].format(
3444
- "Unknown error"
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(self._handle, stream_obj._stream)
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(new_ptr, "Failed to load archive into builder")
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(result,
3543
- "Error during signing", check=lambda r: r < 0)
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(format, source_stream, dest_stream)
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(result,
3769
- "Failed to format embeddable manifest", check=lambda r: r < 0)
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
- "Failed to sign data with Ed25519")
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.11
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("0.85.1", sdk_version())
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