c2pa-python 0.32.13__tar.gz → 0.34.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {c2pa_python-0.32.13/src/c2pa_python.egg-info → c2pa_python-0.34.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/scripts/download_artifacts.py +1 -1
  4. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/setup.py +2 -1
  5. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa/__init__.py +0 -2
  6. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa/c2pa.py +81 -282
  7. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa/lib.py +5 -2
  8. {c2pa_python-0.32.13 → c2pa_python-0.34.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  9. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/tests/test_unit_tests.py +14 -487
  10. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/LICENSE-APACHE +0 -0
  11. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/LICENSE-MIT +0 -0
  12. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/MANIFEST.in +0 -0
  13. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/README.md +0 -0
  14. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/requirements.txt +0 -0
  15. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/setup.cfg +0 -0
  16. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa/build.py +0 -0
  17. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.32.13 → c2pa_python-0.34.0}/tests/test_unit_tests_threaded.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.32.13
3
+ Version: 0.34.0
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "c2pa-python"
7
- version = "0.32.13"
7
+ version = "0.34.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Python bindings for the C2PA Content Authenticity Initiative (CAI) library"
10
10
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -91,7 +91,7 @@ def get_platform_identifier(target_arch=None):
91
91
  else:
92
92
  return "universal-apple-darwin"
93
93
  elif system == "windows":
94
- if target_arch == "arm64":
94
+ if target_arch == "arm64" or machine in ["arm64", "aarch64"]:
95
95
  return "aarch64-pc-windows-msvc"
96
96
  else:
97
97
  return "x86_64-pc-windows-msvc"
@@ -41,6 +41,7 @@ PLATFORM_FOLDERS = {
41
41
  'aarch64-apple-darwin': 'dylib',
42
42
  'x86_64-apple-darwin': 'dylib',
43
43
  'x86_64-pc-windows-msvc': 'dll',
44
+ 'aarch64-pc-windows-msvc': 'dll',
44
45
  'x86_64-unknown-linux-gnu': 'so',
45
46
  'aarch64-unknown-linux-gnu': 'so',
46
47
  }
@@ -78,7 +79,7 @@ def get_platform_identifier(target_arch=None) -> str:
78
79
  else:
79
80
  return "universal-apple-darwin"
80
81
  elif system == "windows":
81
- if target_arch == "arm64":
82
+ if target_arch == "arm64" or platform.machine().lower() in ["arm64", "aarch64"]:
82
83
  return "aarch64-pc-windows-msvc"
83
84
  else:
84
85
  return "x86_64-pc-windows-msvc"
@@ -32,7 +32,6 @@ from .c2pa import (
32
32
  ContextBuilder,
33
33
  ContextProvider,
34
34
  sdk_version,
35
- read_ingredient_file,
36
35
  load_settings
37
36
  ) # NOQA
38
37
 
@@ -52,6 +51,5 @@ __all__ = [
52
51
  'ContextBuilder',
53
52
  'ContextProvider',
54
53
  'sdk_version',
55
- 'read_ingredient_file',
56
54
  'load_settings'
57
55
  ]
@@ -38,8 +38,6 @@ _REQUIRED_FUNCTIONS = [
38
38
  'c2pa_error',
39
39
  # Legacy APIs, deprecated
40
40
  'c2pa_load_settings',
41
- 'c2pa_read_file',
42
- 'c2pa_read_ingredient_file',
43
41
  # Stream
44
42
  'c2pa_create_stream',
45
43
  'c2pa_release_stream',
@@ -48,6 +46,7 @@ _REQUIRED_FUNCTIONS = [
48
46
  'c2pa_reader_from_manifest_data_and_stream',
49
47
  'c2pa_reader_json',
50
48
  'c2pa_reader_detailed_json',
49
+ 'c2pa_reader_crjson',
51
50
  'c2pa_reader_resource_to_stream',
52
51
  'c2pa_reader_from_context',
53
52
  'c2pa_reader_with_stream',
@@ -242,8 +241,13 @@ class ManagedResource:
242
241
 
243
242
  @staticmethod
244
243
  def _free_native_ptr(ptr):
245
- """Free a native pointer by casting it to c_void_p and calling c2pa_free."""
246
- _lib.c2pa_free(ctypes.cast(ptr, ctypes.c_void_p))
244
+ """Free a native pointer by passing it to c2pa_free.
245
+
246
+ c2pa_free's argtype is c_void_p, so ctypes converts any pointer
247
+ instance directly. (ctypes.cast(ptr, c_void_p) would do the same
248
+ conversion but leaves a reference cycle behind on every call.)
249
+ """
250
+ _lib.c2pa_free(ptr)
247
251
 
248
252
  def _ensure_valid_state(self):
249
253
  """Raise if the resource is closed or uninitialized."""
@@ -561,6 +565,9 @@ _setup_function(
561
565
  _setup_function(
562
566
  _lib.c2pa_reader_detailed_json, [
563
567
  ctypes.POINTER(C2paReader)], ctypes.c_void_p)
568
+ _setup_function(
569
+ _lib.c2pa_reader_crjson, [
570
+ ctypes.POINTER(C2paReader)], ctypes.c_void_p)
564
571
  _setup_function(_lib.c2pa_reader_resource_to_stream, [ctypes.POINTER(
565
572
  C2paReader), ctypes.c_char_p, ctypes.POINTER(C2paStream)], ctypes.c_int64)
566
573
  _setup_function(
@@ -641,12 +648,6 @@ _setup_function(_lib.c2pa_signer_create,
641
648
  _setup_function(_lib.c2pa_signer_from_info,
642
649
  [ctypes.POINTER(C2paSignerInfo)],
643
650
  ctypes.POINTER(C2paSigner))
644
- _setup_function(
645
- _lib.c2pa_read_file, [
646
- ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
647
- _setup_function(
648
- _lib.c2pa_read_ingredient_file, [
649
- ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
650
651
 
651
652
  # Set up Signer function prototypes
652
653
  _setup_function(
@@ -885,33 +886,51 @@ class _StringContainer:
885
886
  self._data_dir_str = ""
886
887
 
887
888
 
889
+ if sys.implementation.name == "cpython":
890
+ _PyMemoryView_FromMemory = ctypes.pythonapi.PyMemoryView_FromMemory
891
+ _PyMemoryView_FromMemory.restype = ctypes.py_object
892
+ _PyMemoryView_FromMemory.argtypes = (
893
+ ctypes.c_void_p, ctypes.c_ssize_t, ctypes.c_int)
894
+ _PyBUF_WRITE = 0x200
895
+
896
+ def _write_buf(address, length):
897
+ return _PyMemoryView_FromMemory(address, length, _PyBUF_WRITE)
898
+ else:
899
+ def _write_buf(address, length):
900
+ return (ctypes.c_char * length).from_address(address)
901
+
902
+
888
903
  def _convert_to_py_string(value) -> str:
889
904
  if value is None:
890
905
  return ""
891
906
 
892
907
  py_string = ""
893
908
 
894
- # Validate pointer before casting and freeing
895
- if not isinstance(value, (int, ctypes.c_void_p)) or value == 0:
909
+ # Validate and normalize pointer before reading and freeing.
910
+ if isinstance(value, ctypes.c_void_p):
911
+ address = value.value
912
+ elif isinstance(value, int):
913
+ address = value
914
+ else:
915
+ return ""
916
+ if not address:
896
917
  return ""
897
918
 
898
919
  try:
899
- ptr = ctypes.cast(value, ctypes.c_char_p)
920
+ raw = ctypes.string_at(address)
900
921
 
901
- # Only if we got a valid pointer with valid content
902
- if ptr and ptr.value is not None:
922
+ try:
923
+ py_string = raw.decode('utf-8', errors='strict')
924
+ except Exception:
925
+ py_string = ""
926
+ finally:
927
+ # Only free if we have a valid pointer
903
928
  try:
904
- py_string = ptr.value.decode('utf-8', errors='strict')
929
+ _lib.c2pa_string_free(value)
905
930
  except Exception:
906
- py_string = ""
907
- finally:
908
- # Only free if we have a valid pointer
909
- try:
910
- _lib.c2pa_string_free(value)
911
- except Exception:
912
- # Ignore clean up issues
913
- pass
914
- except (ctypes.ArgumentError, TypeError, ValueError):
931
+ # Ignore clean up issues
932
+ pass
933
+ except (ctypes.ArgumentError, TypeError, ValueError, OSError):
915
934
  # Invalid pointer type or value
916
935
  return ""
917
936
 
@@ -999,8 +1018,7 @@ def _parse_operation_result_for_error(
999
1018
  if check_error:
1000
1019
  error = _lib.c2pa_error()
1001
1020
  if error:
1002
- error_str = ctypes.cast(
1003
- error, ctypes.c_char_p).value.decode('utf-8')
1021
+ error_str = ctypes.string_at(error).decode('utf-8')
1004
1022
  _lib.c2pa_string_free(error)
1005
1023
  _raise_typed_c2pa_error(error_str)
1006
1024
  return None
@@ -1187,208 +1205,6 @@ def _get_mime_type_from_path(path: Union[str, Path]) -> str:
1187
1205
  return mime_type
1188
1206
 
1189
1207
 
1190
- def read_ingredient_file(
1191
- path: Union[str, Path], data_dir: Union[str, Path]) -> str:
1192
- """Read a file as C2PA ingredient (deprecated).
1193
- This creates the JSON string that would be used as the ingredient JSON.
1194
-
1195
- .. deprecated:: 0.11.0
1196
- This function is deprecated and will be removed in a future version.
1197
- To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
1198
- To add ingredients to a manifest,
1199
- use :meth:`c2pa.c2pa.Builder.add_ingredient` instead.
1200
-
1201
- Args:
1202
- path: Path to the file to read
1203
- data_dir: Directory to write binary resources to
1204
-
1205
- Returns:
1206
- The ingredient as a JSON string
1207
-
1208
- Raises:
1209
- C2paError: If there was an error reading the file
1210
- """
1211
- warnings.warn(
1212
- "The read_ingredient_file function is deprecated and will be "
1213
- "removed in a future version. Please use Reader(path).json() for "
1214
- "reading C2PA metadata instead, or "
1215
- "Builder.add_ingredient(json, format, stream) to add ingredients "
1216
- "to a manifest.",
1217
- DeprecationWarning,
1218
- stacklevel=2,
1219
- )
1220
-
1221
- _clear_error_state()
1222
-
1223
- container = _StringContainer()
1224
-
1225
- container._path_str = str(path).encode('utf-8')
1226
- container._data_dir_str = str(data_dir).encode('utf-8')
1227
-
1228
- result = _lib.c2pa_read_ingredient_file(
1229
- container._path_str, container._data_dir_str)
1230
-
1231
- _check_ffi_operation_result(
1232
- result, "Error reading ingredient file {}".format(path))
1233
-
1234
- return _convert_to_py_string(result)
1235
-
1236
-
1237
- def read_file(path: Union[str, Path],
1238
- data_dir: Union[str, Path]) -> str:
1239
- """Read a C2PA manifest from a file (deprecated).
1240
-
1241
- .. deprecated:: 0.10.0
1242
- This function is deprecated and will be removed in a future version.
1243
- To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
1244
-
1245
- Args:
1246
- path: Path to the file to read
1247
- data_dir: Directory to write binary resources to
1248
-
1249
- Returns:
1250
- The manifest as a JSON string
1251
-
1252
- Raises:
1253
- C2paError: If there was an error reading the file
1254
- """
1255
- warnings.warn(
1256
- "The read_file function is deprecated and will be removed in a "
1257
- "future version. Please use the Reader class for reading C2PA "
1258
- "metadata instead.",
1259
- DeprecationWarning,
1260
- stacklevel=2,
1261
- )
1262
-
1263
- _clear_error_state()
1264
-
1265
- container = _StringContainer()
1266
-
1267
- container._path_str = str(path).encode('utf-8')
1268
- container._data_dir_str = str(data_dir).encode('utf-8')
1269
-
1270
- result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
1271
- _check_ffi_operation_result(
1272
- result, "Error during read of manifest from file {}".format(path))
1273
-
1274
- return _convert_to_py_string(result)
1275
-
1276
-
1277
- @overload
1278
- def sign_file(
1279
- source_path: Union[str, Path],
1280
- dest_path: Union[str, Path],
1281
- manifest: str,
1282
- signer_info: C2paSignerInfo,
1283
- return_manifest_as_bytes: bool = False
1284
- ) -> Union[str, bytes]:
1285
- """Sign a file with a C2PA manifest using signer info.
1286
- """
1287
- ...
1288
-
1289
-
1290
- @overload
1291
- def sign_file(
1292
- source_path: Union[str, Path],
1293
- dest_path: Union[str, Path],
1294
- manifest: str,
1295
- signer: 'Signer',
1296
- return_manifest_as_bytes: bool = False
1297
- ) -> Union[str, bytes]:
1298
- """Sign a file with a C2PA manifest using a signer.
1299
- """
1300
- ...
1301
-
1302
-
1303
- def sign_file(
1304
- source_path: Union[str, Path],
1305
- dest_path: Union[str, Path],
1306
- manifest: str,
1307
- signer_or_info: Union[C2paSignerInfo, 'Signer'],
1308
- return_manifest_as_bytes: bool = False
1309
- ) -> Union[str, bytes]:
1310
- """Sign a file with a C2PA manifest (deprecated).
1311
- For now, this function is left here to provide a backwards-compatible API.
1312
-
1313
- .. deprecated:: 0.13.0
1314
- This function is deprecated and will be removed in a future version.
1315
- Use :meth:`Builder.sign` instead.
1316
-
1317
- Args:
1318
- source_path: Path to the source file. We will attempt
1319
- to guess the mimetype of the source file based on
1320
- the extension.
1321
- dest_path: Path to write the signed file to
1322
- manifest: The manifest JSON string
1323
- signer_or_info: Either a signer configuration or a signer object
1324
- return_manifest_as_bytes: If True, return manifest bytes instead
1325
- of JSON string
1326
-
1327
- Returns:
1328
- The signed manifest as a JSON string or bytes, depending
1329
- on return_manifest_as_bytes
1330
-
1331
- Raises:
1332
- C2paError: If there was an error signing the file
1333
- C2paError.Encoding: If any of the string inputs contain
1334
- invalid UTF-8 characters
1335
- C2paError.NotSupported: If the file type cannot be determined
1336
- """
1337
-
1338
- warnings.warn(
1339
- "The sign_file function is deprecated and will be removed in a "
1340
- "future version. Please use the Builder object and Builder.sign() "
1341
- "instead.",
1342
- DeprecationWarning,
1343
- stacklevel=2,
1344
- )
1345
-
1346
- _clear_error_state()
1347
-
1348
- try:
1349
- # Determine if we have a signer or signer info
1350
- if isinstance(signer_or_info, C2paSignerInfo):
1351
- signer = Signer.from_info(signer_or_info)
1352
- own_signer = True
1353
- else:
1354
- signer = signer_or_info
1355
- own_signer = False
1356
-
1357
- # Create a builder from the manifest
1358
- builder = Builder(manifest)
1359
-
1360
- manifest_bytes = builder.sign_file(
1361
- source_path,
1362
- dest_path,
1363
- signer
1364
- )
1365
-
1366
- if return_manifest_as_bytes:
1367
- return manifest_bytes
1368
- else:
1369
- # Read the signed manifest from the destination file
1370
- with Reader(dest_path) as reader:
1371
- return reader.json()
1372
-
1373
- except Exception as e:
1374
- # Clean up destination file if it exists and there was an error
1375
- if os.path.exists(dest_path):
1376
- try:
1377
- os.remove(dest_path)
1378
- except OSError:
1379
- logger.warning("Failed to remove destination file")
1380
- pass # Ignore cleanup errors
1381
-
1382
- # Re-raise the error
1383
- raise C2paError(f"Error signing file: {str(e)}") from e
1384
- finally:
1385
- # Ensure resources are cleaned up
1386
- if 'builder' in locals():
1387
- builder.close()
1388
- if 'signer' in locals() and own_signer:
1389
- signer.close()
1390
-
1391
-
1392
1208
  class ContextProvider(ABC):
1393
1209
  """Abstract base class for types that provide a C2PA context.
1394
1210
 
@@ -1823,10 +1639,21 @@ class Stream:
1823
1639
  readinto = getattr(stream, "readinto", None)
1824
1640
  if readinto is not None:
1825
1641
  # 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
1642
+ buf = _write_buf(
1643
+ ctypes.addressof(data.contents), length)
1644
+ try:
1645
+ n = readinto(buf)
1646
+ finally:
1647
+ release = getattr(buf, "release", None)
1648
+ if release is not None:
1649
+ release()
1650
+ if not n:
1651
+ return 0
1652
+ if n > length:
1653
+ raise ValueError(
1654
+ f"readinto returned {n} bytes but buffer length is {length}"
1655
+ )
1656
+ return n
1830
1657
 
1831
1658
  # Fallback for streams without readinto.
1832
1659
  buffer = stream.read(length)
@@ -2631,6 +2458,27 @@ class Reader(ManagedResource):
2631
2458
 
2632
2459
  return _convert_to_py_string(result)
2633
2460
 
2461
+ def crjson(self) -> str:
2462
+ """Get the manifest store as a crJSON string.
2463
+
2464
+ crJSON is a standardized JSON format for C2PA manifest data. This
2465
+ call yields empty JSON ("{}") when there are no Content Credentials.
2466
+
2467
+ Returns:
2468
+ The manifest store as a crJSON string.
2469
+
2470
+ Raises:
2471
+ C2paError: If the Reader has been closed or the underlying C
2472
+ call returns null.
2473
+ """
2474
+
2475
+ self._ensure_valid_state()
2476
+
2477
+ result = _lib.c2pa_reader_crjson(self._handle)
2478
+ _check_ffi_operation_result(result, "Error parsing crJSON")
2479
+
2480
+ return _convert_to_py_string(result)
2481
+
2634
2482
  def _get_manifest_field(self, extractor):
2635
2483
  """Extract a field from (cached) manifest data, or None if unavailable.
2636
2484
 
@@ -3386,52 +3234,6 @@ class Builder(ManagedResource):
3386
3234
  Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"),
3387
3235
  check=lambda r: r != 0)
3388
3236
 
3389
- def add_ingredient_from_file_path(
3390
- self,
3391
- ingredient_json: Union[str, dict],
3392
- format: str,
3393
- filepath: Union[str, Path]):
3394
- """Add an ingredient from a file path to the builder (deprecated).
3395
- This is a legacy method.
3396
-
3397
- .. deprecated:: 0.13.0
3398
- This method is deprecated and will be removed in a future version.
3399
- Use :meth:`add_ingredient` with a file stream instead.
3400
-
3401
- Args:
3402
- ingredient_json: The JSON ingredient definition
3403
- (either a JSON string or a dictionary)
3404
- format: The MIME type or extension of the ingredient
3405
- filepath: The path to the file containing the ingredient data
3406
- (can be a string or Path object)
3407
-
3408
- Raises:
3409
- C2paError: If there was an error adding the ingredient
3410
- C2paError.Encoding: If the ingredient JSON or format
3411
- contains invalid UTF-8 characters
3412
- FileNotFoundError: If the file at the specified path does not exist
3413
- """
3414
- warnings.warn(
3415
- "add_ingredient_from_file_path is deprecated and will "
3416
- "be removed in a future version. Use add_ingredient "
3417
- "with a file stream instead.",
3418
- DeprecationWarning,
3419
- stacklevel=2,
3420
- )
3421
-
3422
- try:
3423
- # Convert Path object to string if necessary
3424
- filepath_str = str(filepath)
3425
-
3426
- # Does the stream handling to use add_ingredient_from_stream
3427
- with open(filepath_str, 'rb') as file_stream:
3428
- self.add_ingredient_from_stream(
3429
- ingredient_json, format, file_stream)
3430
- except FileNotFoundError as e:
3431
- raise C2paError.FileNotFound(f"File not found: {filepath}") from e
3432
- except Exception as e:
3433
- raise C2paError.Other(f"Could not add ingredient: {e}") from e
3434
-
3435
3237
  def add_action(self, action_json: Union[str, dict]) -> None:
3436
3238
  """Add an action to the builder, that will be placed
3437
3239
  in the actions assertion array in the generated manifest.
@@ -3966,9 +3768,6 @@ __all__ = [
3966
3768
  'Builder',
3967
3769
  'Signer',
3968
3770
  'load_settings',
3969
- 'read_file',
3970
- 'read_ingredient_file',
3971
- 'sign_file',
3972
3771
  'format_embeddable',
3973
3772
  'version',
3974
3773
  'sdk_version'
@@ -37,7 +37,8 @@ def get_platform_identifier() -> str:
37
37
  - universal-apple-darwin (for Mac universal)
38
38
  - aarch64-apple-darwin (for Mac ARM64)
39
39
  - x86_64-apple-darwin (for Mac x86_64)
40
- - x86_64-pc-windows-msvc (for Windows 64-bit)
40
+ - x86_64-pc-windows-msvc (for Windows x64)
41
+ - aarch64-pc-windows-msvc (for Windows ARM64)
41
42
  - x86_64-unknown-linux-gnu (for Linux 64-bit)
42
43
  - aarch64-unknown-linux-gnu (for Linux ARM)
43
44
  """
@@ -54,6 +55,8 @@ def get_platform_identifier() -> str:
54
55
  else:
55
56
  return "universal-apple-darwin"
56
57
  elif system == "windows":
58
+ if _get_architecture() in [CPUArchitecture.ARM64.value, CPUArchitecture.AARCH64.value]:
59
+ return "aarch64-pc-windows-msvc"
57
60
  return "x86_64-pc-windows-msvc"
58
61
  elif system == "linux":
59
62
  if _get_architecture() in [CPUArchitecture.ARM64.value, CPUArchitecture.AARCH64.value]:
@@ -81,7 +84,7 @@ def _get_architecture() -> str:
81
84
  elif sys.platform == "win32":
82
85
  # win32 will cover all Windows versions
83
86
  # (the 32 is a historical quirk)
84
- return platform.machine()
87
+ return platform.machine().lower()
85
88
  else:
86
89
  raise RuntimeError(f"Unsupported platform: {sys.platform}")
87
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.32.13
3
+ Version: 0.34.0
4
4
  Summary: Python bindings for the C2PA Content Authenticity Initiative (CAI) library
5
5
  Author-email: Gavin Peacock <gvnpeacock@adobe.com>, Tania Mathern <mathern@adobe.com>
6
6
  Maintainer-email: Gavin Peacock <gpeacock@adobe.com>
@@ -31,7 +31,7 @@ warnings.simplefilter("ignore", category=DeprecationWarning)
31
31
 
32
32
  from c2pa import Builder, C2paError as Error, Reader, C2paSigningAlg as SigningAlg, C2paSignerInfo, Signer, sdk_version, C2paBuilderIntent, C2paDigitalSourceType
33
33
  from c2pa import Settings, Context, ContextBuilder, ContextProvider
34
- from c2pa.c2pa import Stream, LifecycleState, read_ingredient_file, read_file, sign_file, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable
34
+ from c2pa.c2pa import Stream, LifecycleState, load_settings, create_signer, create_signer_from_info, ed25519_sign, format_embeddable
35
35
 
36
36
 
37
37
  PROJECT_PATH = os.getcwd()
@@ -271,6 +271,19 @@ class TestReader(unittest.TestCase):
271
271
  title = manifest_store["manifests"][manifest_store["active_manifest"]]["claim"]["dc:title"]
272
272
  self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
273
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
+
274
287
  def test_stream_read_string_stream_path_only(self):
275
288
  with Reader(self.testPath) as reader:
276
289
  json_data = reader.json()
@@ -4774,9 +4787,6 @@ class TestStream(unittest.TestCase):
4774
4787
  class TestLegacyAPI(unittest.TestCase):
4775
4788
  def setUp(self):
4776
4789
  # Filter specific deprecation warnings for legacy API tests
4777
- warnings.filterwarnings("ignore", message="The read_file function is deprecated")
4778
- warnings.filterwarnings("ignore", message="The sign_file function is deprecated")
4779
- warnings.filterwarnings("ignore", message="The read_ingredient_file function is deprecated")
4780
4790
  warnings.filterwarnings("ignore", message="The create_signer function is deprecated")
4781
4791
  warnings.filterwarnings("ignore", message="The create_signer_from_info function is deprecated")
4782
4792
  warnings.filterwarnings("ignore", message="load_settings\\(\\) is deprecated")
@@ -4850,489 +4860,6 @@ class TestLegacyAPI(unittest.TestCase):
4850
4860
  if os.path.exists(self.temp_data_dir):
4851
4861
  shutil.rmtree(self.temp_data_dir)
4852
4862
 
4853
- def test_read_ingredient_file(self):
4854
- """Test reading a C2PA ingredient from a file."""
4855
- # Test reading ingredient from file with data_dir
4856
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4857
- os.makedirs(temp_data_dir, exist_ok=True)
4858
-
4859
- ingredient_json_with_dir = read_ingredient_file(self.testPath, temp_data_dir)
4860
-
4861
- # Verify some fields
4862
- ingredient_data = json.loads(ingredient_json_with_dir)
4863
- self.assertEqual(ingredient_data["title"], DEFAULT_TEST_FILE_NAME)
4864
- self.assertEqual(ingredient_data["format"], "image/jpeg")
4865
- self.assertIn("thumbnail", ingredient_data)
4866
-
4867
- def test_read_ingredient_file_who_has_no_manifest(self):
4868
- """Test reading a C2PA ingredient from a file."""
4869
- # Test reading ingredient from file with data_dir
4870
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4871
- os.makedirs(temp_data_dir, exist_ok=True)
4872
-
4873
- # Load settings first, before they need to be used
4874
- load_settings('{"builder": { "thumbnail": {"enabled": false}}}')
4875
-
4876
- ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
4877
-
4878
- # Verify some fields
4879
- ingredient_data = json.loads(ingredient_json_with_dir)
4880
- self.assertEqual(ingredient_data["title"], INGREDIENT_TEST_FILE_NAME)
4881
- self.assertEqual(ingredient_data["format"], "image/jpeg")
4882
- self.assertNotIn("thumbnail", ingredient_data)
4883
-
4884
- # Reset setting
4885
- load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
4886
-
4887
- def test_compare_read_ingredient_file_with_builder_added_ingredient(self):
4888
- """Test reading a C2PA ingredient from a file."""
4889
- # Test reading ingredient from file with data_dir
4890
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4891
- os.makedirs(temp_data_dir, exist_ok=True)
4892
-
4893
- ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
4894
-
4895
- # Ingredient fields from read_ingredient_file
4896
- ingredient_data = json.loads(ingredient_json_with_dir)
4897
-
4898
- # Compare with ingredient added by Builder
4899
- builder = Builder.from_json(self.manifestDefinition)
4900
- # Only the title is needed (filename), since title not extracted or guessed from filename
4901
- ingredient_json = '{ "title" : "A.jpg" }'
4902
- with open(self.testPath2, 'rb') as f:
4903
- builder.add_ingredient(ingredient_json, "image/jpeg", f)
4904
-
4905
- with open(self.testPath2, "rb") as file:
4906
- output = io.BytesIO(bytearray())
4907
- builder.sign(self.signer, "image/jpeg", file, output)
4908
- output.seek(0)
4909
-
4910
- # Get ingredient fields from signed manifest
4911
- reader = Reader("image/jpeg", output)
4912
- json_data = reader.json()
4913
- manifest_data = json.loads(json_data)
4914
- active_manifest_id = manifest_data["active_manifest"]
4915
- active_manifest = manifest_data["manifests"][active_manifest_id]
4916
- only_ingredient = active_manifest["ingredients"][0]
4917
-
4918
- self.assertEqual(ingredient_data["title"], only_ingredient["title"])
4919
- self.assertEqual(ingredient_data["format"], only_ingredient["format"])
4920
- self.assertEqual(ingredient_data["document_id"], only_ingredient["document_id"])
4921
- self.assertEqual(ingredient_data["instance_id"], only_ingredient["instance_id"])
4922
- self.assertEqual(ingredient_data["relationship"], only_ingredient["relationship"])
4923
-
4924
- def test_read_file(self):
4925
- """Test reading a C2PA ingredient from a file."""
4926
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4927
- os.makedirs(temp_data_dir, exist_ok=True)
4928
-
4929
- # self.testPath has C2PA metadata to read
4930
- file_json_with_dir = read_file(self.testPath, temp_data_dir)
4931
-
4932
- # Parse the JSON and verify specific fields
4933
- file_data = json.loads(file_json_with_dir)
4934
- expected_manifest_id = "contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e"
4935
-
4936
- # Verify some fields
4937
- self.assertEqual(file_data["active_manifest"], expected_manifest_id)
4938
- self.assertIn("manifests", file_data)
4939
- self.assertIn(expected_manifest_id, file_data["manifests"])
4940
-
4941
- def test_sign_file(self):
4942
- """Test signing a file with C2PA manifest."""
4943
- # Set up test paths
4944
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4945
- os.makedirs(temp_data_dir, exist_ok=True)
4946
- output_path = os.path.join(temp_data_dir, "signed_output.jpg")
4947
-
4948
- # Load test certificates and key
4949
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
4950
- certs = cert_file.read()
4951
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
4952
- key = key_file.read()
4953
-
4954
- # Create signer info
4955
- signer_info = C2paSignerInfo(
4956
- alg=b"es256",
4957
- sign_cert=certs,
4958
- private_key=key,
4959
- ta_url=b"http://timestamp.digicert.com"
4960
- )
4961
-
4962
- # Create a simple manifest
4963
- manifest = {
4964
- "claim_generator": "python_internals_test",
4965
- "claim_generator_info": [{
4966
- "name": "python_internals_test",
4967
- "version": "0.0.1",
4968
- }],
4969
- # Claim version has become mandatory for signing v1 claims
4970
- "claim_version": 1,
4971
- "format": "image/jpeg",
4972
- "title": "Python Test Signed Image",
4973
- "ingredients": [],
4974
- "assertions": [
4975
- {
4976
- "label": "c2pa.actions",
4977
- "data": {
4978
- "actions": [
4979
- {
4980
- "action": "c2pa.opened"
4981
- }
4982
- ]
4983
- }
4984
- }
4985
- ]
4986
- }
4987
-
4988
- # Convert manifest to JSON string
4989
- manifest_json = json.dumps(manifest)
4990
-
4991
- try:
4992
- sign_file(
4993
- self.testPath,
4994
- output_path,
4995
- manifest_json,
4996
- signer_info
4997
- )
4998
-
4999
- finally:
5000
- # Clean up
5001
- if os.path.exists(output_path):
5002
- os.remove(output_path)
5003
-
5004
- def test_sign_file_does_not_exist_errors(self):
5005
- """Test signing a file with C2PA manifest."""
5006
- # Set up test paths
5007
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
5008
- os.makedirs(temp_data_dir, exist_ok=True)
5009
- output_path = os.path.join(temp_data_dir, "signed_output.jpg")
5010
-
5011
- # Load test certificates and key
5012
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
5013
- certs = cert_file.read()
5014
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
5015
- key = key_file.read()
5016
-
5017
- # Create signer info
5018
- signer_info = C2paSignerInfo(
5019
- alg=b"es256",
5020
- sign_cert=certs,
5021
- private_key=key,
5022
- ta_url=b"http://timestamp.digicert.com"
5023
- )
5024
-
5025
- # Create a simple manifest
5026
- manifest = {
5027
- "claim_generator": "python_internals_test",
5028
- "claim_generator_info": [{
5029
- "name": "python_internals_test",
5030
- "version": "0.0.1",
5031
- }],
5032
- # Claim version has become mandatory for signing v1 claims
5033
- "claim_version": 1,
5034
- "format": "image/jpeg",
5035
- "title": "Python Test Signed Image",
5036
- "ingredients": [],
5037
- "assertions": [
5038
- {
5039
- "label": "c2pa.actions",
5040
- "data": {
5041
- "actions": [
5042
- {
5043
- "action": "c2pa.opened"
5044
- }
5045
- ]
5046
- }
5047
- }
5048
- ]
5049
- }
5050
-
5051
- # Convert manifest to JSON string
5052
- manifest_json = json.dumps(manifest)
5053
-
5054
- try:
5055
- with self.assertRaises(Error):
5056
- sign_file(
5057
- "this-file-does-not-exist",
5058
- output_path,
5059
- manifest_json,
5060
- signer_info
5061
- )
5062
-
5063
- finally:
5064
- # Clean up
5065
- if os.path.exists(output_path):
5066
- os.remove(output_path)
5067
-
5068
- def test_builder_sign_with_ingredient_from_file(self):
5069
- """Test Builder class operations with an ingredient added from file path."""
5070
-
5071
- builder = Builder.from_json(self.manifestDefinition)
5072
-
5073
- # Test adding ingredient from file path
5074
- ingredient_json = '{"title": "Test Ingredient From File"}'
5075
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5076
- with warnings.catch_warnings():
5077
- warnings.simplefilter("ignore", DeprecationWarning)
5078
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath3)
5079
-
5080
- with open(self.testPath2, "rb") as file:
5081
- output = io.BytesIO(bytearray())
5082
- builder.sign(self.signer, "image/jpeg", file, output)
5083
- output.seek(0)
5084
- reader = Reader("image/jpeg", output)
5085
- json_data = reader.json()
5086
- manifest_data = json.loads(json_data)
5087
-
5088
- # Verify active manifest exists
5089
- self.assertIn("active_manifest", manifest_data)
5090
- active_manifest_id = manifest_data["active_manifest"]
5091
-
5092
- # Verify active manifest object exists
5093
- self.assertIn("manifests", manifest_data)
5094
- self.assertIn(active_manifest_id, manifest_data["manifests"])
5095
- active_manifest = manifest_data["manifests"][active_manifest_id]
5096
-
5097
- # Verify ingredients array exists in active manifest
5098
- self.assertIn("ingredients", active_manifest)
5099
- self.assertIsInstance(active_manifest["ingredients"], list)
5100
- self.assertTrue(len(active_manifest["ingredients"]) > 0)
5101
-
5102
- # Verify the first ingredient's title matches what we set
5103
- first_ingredient = active_manifest["ingredients"][0]
5104
- self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
5105
-
5106
- builder.close()
5107
-
5108
- def test_builder_sign_with_ingredient_dict_from_file(self):
5109
- """Test Builder class operations with an ingredient added from file path using a dictionary."""
5110
-
5111
- builder = Builder.from_json(self.manifestDefinition)
5112
-
5113
- # Test adding ingredient from file path with a dictionary
5114
- ingredient_dict = {"title": "Test Ingredient From File"}
5115
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5116
- with warnings.catch_warnings():
5117
- warnings.simplefilter("ignore", DeprecationWarning)
5118
- builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3)
5119
-
5120
- with open(self.testPath2, "rb") as file:
5121
- output = io.BytesIO(bytearray())
5122
- builder.sign(self.signer, "image/jpeg", file, output)
5123
- output.seek(0)
5124
- reader = Reader("image/jpeg", output)
5125
- json_data = reader.json()
5126
- manifest_data = json.loads(json_data)
5127
-
5128
- # Verify active manifest exists
5129
- self.assertIn("active_manifest", manifest_data)
5130
- active_manifest_id = manifest_data["active_manifest"]
5131
-
5132
- # Verify active manifest object exists
5133
- self.assertIn("manifests", manifest_data)
5134
- self.assertIn(active_manifest_id, manifest_data["manifests"])
5135
- active_manifest = manifest_data["manifests"][active_manifest_id]
5136
-
5137
- # Verify ingredients array exists in active manifest
5138
- self.assertIn("ingredients", active_manifest)
5139
- self.assertIsInstance(active_manifest["ingredients"], list)
5140
- self.assertTrue(len(active_manifest["ingredients"]) > 0)
5141
-
5142
- # Verify the first ingredient's title matches what we set
5143
- first_ingredient = active_manifest["ingredients"][0]
5144
- self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
5145
-
5146
- builder.close()
5147
-
5148
- def test_builder_add_ingredient_from_file_path(self):
5149
- """Test Builder class add_ingredient_from_file_path method."""
5150
-
5151
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5152
- with warnings.catch_warnings():
5153
- warnings.simplefilter("ignore", DeprecationWarning)
5154
-
5155
- builder = Builder.from_json(self.manifestDefinition)
5156
-
5157
- # Test adding ingredient from file path
5158
- ingredient_json = '{"test": "ingredient_from_file_path"}'
5159
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath)
5160
-
5161
- builder.close()
5162
-
5163
- def test_builder_add_ingredient_from_file_path_not_found(self):
5164
- """Test Builder class add_ingredient_from_file_path method."""
5165
-
5166
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5167
- with warnings.catch_warnings():
5168
- warnings.simplefilter("ignore", DeprecationWarning)
5169
-
5170
- builder = Builder.from_json(self.manifestDefinition)
5171
-
5172
- # Test adding ingredient from file path
5173
- ingredient_json = '{"test": "ingredient_from_file_path"}'
5174
-
5175
- with self.assertRaises(Error.FileNotFound):
5176
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "this-file-path-does-not-exist")
5177
-
5178
- def test_sign_file_using_callback_signer_overloads(self):
5179
- """Test signing a file using the sign_file function with a Signer object."""
5180
- # Create a temporary directory for the test
5181
- temp_dir = tempfile.mkdtemp()
5182
-
5183
- try:
5184
- # Create a temporary output file path
5185
- output_path = os.path.join(temp_dir, "signed_output_callback.jpg")
5186
-
5187
- # Create signer with callback
5188
- signer = Signer.from_callback(
5189
- callback=self.callback_signer_es256,
5190
- alg=SigningAlg.ES256,
5191
- certs=self.certs.decode('utf-8'),
5192
- tsa_url="http://timestamp.digicert.com"
5193
- )
5194
-
5195
- # Overload that returns a JSON string
5196
- result_json = sign_file(
5197
- self.testPath,
5198
- output_path,
5199
- self.manifestDefinition,
5200
- signer,
5201
- False
5202
- )
5203
-
5204
- # Verify the output file was created
5205
- self.assertTrue(os.path.exists(output_path))
5206
-
5207
- # Verify the result is JSON
5208
- self.assertIsInstance(result_json, str)
5209
- self.assertGreater(len(result_json), 0)
5210
-
5211
- manifest_data = json.loads(result_json)
5212
- self.assertIn("manifests", manifest_data)
5213
- self.assertIn("active_manifest", manifest_data)
5214
-
5215
- output_path_bytes = os.path.join(temp_dir, "signed_output_callback_bytes.jpg")
5216
- # Overload that returns bytes
5217
- result_bytes = sign_file(
5218
- self.testPath,
5219
- output_path_bytes,
5220
- self.manifestDefinition,
5221
- signer,
5222
- True
5223
- )
5224
-
5225
- # Verify the output file was created
5226
- self.assertTrue(os.path.exists(output_path_bytes))
5227
-
5228
- # Verify the result is bytes
5229
- self.assertIsInstance(result_bytes, bytes)
5230
- self.assertGreater(len(result_bytes), 0)
5231
-
5232
- # Read the signed file and verify the manifest contains expected content
5233
- with open(output_path, "rb") as file:
5234
- reader = Reader("image/jpeg", file)
5235
- file_manifest_json = reader.json()
5236
- self.assertIn("Python Test", file_manifest_json)
5237
-
5238
- finally:
5239
- shutil.rmtree(temp_dir)
5240
-
5241
- def test_sign_file_overloads(self):
5242
- """Test that the overloaded sign_file function works with both parameter types."""
5243
- # Create a temporary directory for the test
5244
- temp_dir = tempfile.mkdtemp()
5245
- try:
5246
- # Test with C2paSignerInfo
5247
- output_path_1 = os.path.join(temp_dir, "signed_output_1.jpg")
5248
-
5249
- # Load test certificates and key
5250
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
5251
- certs = cert_file.read()
5252
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
5253
- key = key_file.read()
5254
-
5255
- # Create signer info
5256
- signer_info = C2paSignerInfo(
5257
- alg=b"es256",
5258
- sign_cert=certs,
5259
- private_key=key,
5260
- ta_url=b"http://timestamp.digicert.com"
5261
- )
5262
-
5263
- # Test with C2paSignerInfo parameter - JSON return
5264
- result_1 = sign_file(
5265
- self.testPath,
5266
- output_path_1,
5267
- self.manifestDefinition,
5268
- signer_info,
5269
- False
5270
- )
5271
-
5272
- self.assertIsInstance(result_1, str)
5273
- self.assertTrue(os.path.exists(output_path_1))
5274
-
5275
- # Test with C2paSignerInfo parameter - bytes return
5276
- output_path_1_bytes = os.path.join(temp_dir, "signed_output_1_bytes.jpg")
5277
- result_1_bytes = sign_file(
5278
- self.testPath,
5279
- output_path_1_bytes,
5280
- self.manifestDefinition,
5281
- signer_info,
5282
- True
5283
- )
5284
-
5285
- self.assertIsInstance(result_1_bytes, bytes)
5286
- self.assertTrue(os.path.exists(output_path_1_bytes))
5287
-
5288
- # Test with Signer object
5289
- output_path_2 = os.path.join(temp_dir, "signed_output_2.jpg")
5290
-
5291
- # Create a signer from the signer info
5292
- signer = Signer.from_info(signer_info)
5293
-
5294
- # Test with Signer parameter - JSON return
5295
- result_2 = sign_file(
5296
- self.testPath,
5297
- output_path_2,
5298
- self.manifestDefinition,
5299
- signer,
5300
- False
5301
- )
5302
-
5303
- self.assertIsInstance(result_2, str)
5304
- self.assertTrue(os.path.exists(output_path_2))
5305
-
5306
- # Test with Signer parameter - bytes return
5307
- output_path_2_bytes = os.path.join(temp_dir, "signed_output_2_bytes.jpg")
5308
- result_2_bytes = sign_file(
5309
- self.testPath,
5310
- output_path_2_bytes,
5311
- self.manifestDefinition,
5312
- signer,
5313
- True
5314
- )
5315
-
5316
- self.assertIsInstance(result_2_bytes, bytes)
5317
- self.assertTrue(os.path.exists(output_path_2_bytes))
5318
-
5319
- # Both JSON results should be similar (same manifest structure)
5320
- manifest_1 = json.loads(result_1)
5321
- manifest_2 = json.loads(result_2)
5322
-
5323
- self.assertIn("manifests", manifest_1)
5324
- self.assertIn("manifests", manifest_2)
5325
- self.assertIn("active_manifest", manifest_1)
5326
- self.assertIn("active_manifest", manifest_2)
5327
-
5328
- # Both bytes results should be non-empty
5329
- self.assertGreater(len(result_1_bytes), 0)
5330
- self.assertGreater(len(result_2_bytes), 0)
5331
-
5332
- finally:
5333
- # Clean up the temporary directory
5334
- shutil.rmtree(temp_dir)
5335
-
5336
4863
  def test_sign_file_callback_signer_reports_error(self):
5337
4864
  """Test signing a file using the sign_file method with a callback that reports an error."""
5338
4865
 
File without changes
File without changes
File without changes
File without changes