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.
Files changed (22) hide show
  1. {c2pa_python-0.32.12/src/c2pa_python.egg-info → c2pa_python-0.32.14}/PKG-INFO +43 -1
  2. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/README.md +42 -0
  3. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/pyproject.toml +1 -1
  4. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/c2pa.py +122 -87
  5. {c2pa_python-0.32.12 → c2pa_python-0.32.14/src/c2pa_python.egg-info}/PKG-INFO +43 -1
  6. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/tests/test_unit_tests.py +29 -1
  7. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/LICENSE-APACHE +0 -0
  8. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/LICENSE-MIT +0 -0
  9. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/MANIFEST.in +0 -0
  10. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/requirements.txt +0 -0
  11. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/scripts/download_artifacts.py +0 -0
  12. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/setup.cfg +0 -0
  13. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/setup.py +0 -0
  14. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/__init__.py +0 -0
  15. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/build.py +0 -0
  16. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.32.12 → c2pa_python-0.32.14}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {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.12
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.12"
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(f"{name} has an invalid internal state (active but no handle)")
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(result, fallback_msg, *, check=lambda r: not r):
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], error_context: str = "input") -> bytes:
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}, got {type(data).__name__}"
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(result, "Error loading settings", check=lambda r: r != 0)
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(result, "Error reading ingredient file {}".format(path))
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(result, "Error during read of manifest from file {}".format(path))
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 = (ctypes.c_ubyte * len(manifest_data)).from_buffer_copy(
2351
- manifest_data)
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(self._handle,
2362
- Reader._ERROR_MESSAGES['reader_error'].format("Unknown error")
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
- Reader._ERROR_MESSAGES[
2426
- 'reader_error'
2427
- ].format("Unknown error")
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 = (ctypes.c_ubyte * len(manifest_data)).from_buffer_copy(
2440
- manifest_data)
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
- Reader._ERROR_MESSAGES[
2466
- 'reader_error'
2467
- ].format("Unknown error")
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
- Reader._ERROR_MESSAGES[
2560
- 'fragment_error'
2561
- ].format("Unknown error"))
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
- "Error during manifest parsing in Reader")
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(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(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(signer_ptr,
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
- "Failed to create signer")
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(result,
3013
- "Failed to get reserve size", check=lambda r: r < 0)
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
- "Failed to create builder from archive"
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(self._handle,
3182
- Builder._ERROR_MESSAGES['builder_error'].format(
3183
- "Unknown error"
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
- Builder._ERROR_MESSAGES[
3204
- 'builder_error'
3205
- ].format("Unknown error")
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
- Builder._ERROR_MESSAGES[
3219
- 'builder_error'
3220
- ].format("Unknown error")
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(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(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(result,
3311
- Builder._ERROR_MESSAGES['resource_error'].format(
3312
- "Unknown error"
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(result,
3379
- Builder._ERROR_MESSAGES['ingredient_error'].format(
3380
- "Unknown error"
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(result,
3448
- Builder._ERROR_MESSAGES['action_error'].format(
3449
- "Unknown error"
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(result,
3470
- Builder._ERROR_MESSAGES["archive_error"].format(
3471
- "Unknown error"
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(self._handle, stream_obj._stream)
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(new_ptr, "Failed to load archive into builder")
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(result,
3570
- "Error during signing", check=lambda r: r < 0)
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(format, source_stream, dest_stream)
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(result,
3796
- "Failed to format embeddable manifest", check=lambda r: r < 0)
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
- "Failed to sign data with Ed25519")
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.12
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("0.85.1", sdk_version())
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