c2pa-python 0.32.14__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.14/src/c2pa_python.egg-info → c2pa_python-0.34.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/scripts/download_artifacts.py +1 -1
  4. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/setup.py +2 -1
  5. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa/__init__.py +0 -2
  6. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa/c2pa.py +56 -282
  7. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa/lib.py +5 -2
  8. {c2pa_python-0.32.14 → c2pa_python-0.34.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  9. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/tests/test_unit_tests.py +1 -487
  10. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/LICENSE-APACHE +0 -0
  11. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/LICENSE-MIT +0 -0
  12. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/MANIFEST.in +0 -0
  13. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/README.md +0 -0
  14. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/requirements.txt +0 -0
  15. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/setup.cfg +0 -0
  16. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa/build.py +0 -0
  17. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.32.14 → c2pa_python-0.34.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.32.14 → 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.14
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.14"
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',
@@ -243,8 +241,13 @@ class ManagedResource:
243
241
 
244
242
  @staticmethod
245
243
  def _free_native_ptr(ptr):
246
- """Free a native pointer by casting it to c_void_p and calling c2pa_free."""
247
- _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)
248
251
 
249
252
  def _ensure_valid_state(self):
250
253
  """Raise if the resource is closed or uninitialized."""
@@ -645,12 +648,6 @@ _setup_function(_lib.c2pa_signer_create,
645
648
  _setup_function(_lib.c2pa_signer_from_info,
646
649
  [ctypes.POINTER(C2paSignerInfo)],
647
650
  ctypes.POINTER(C2paSigner))
648
- _setup_function(
649
- _lib.c2pa_read_file, [
650
- ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
651
- _setup_function(
652
- _lib.c2pa_read_ingredient_file, [
653
- ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p)
654
651
 
655
652
  # Set up Signer function prototypes
656
653
  _setup_function(
@@ -889,33 +886,51 @@ class _StringContainer:
889
886
  self._data_dir_str = ""
890
887
 
891
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
+
892
903
  def _convert_to_py_string(value) -> str:
893
904
  if value is None:
894
905
  return ""
895
906
 
896
907
  py_string = ""
897
908
 
898
- # Validate pointer before casting and freeing
899
- 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:
900
917
  return ""
901
918
 
902
919
  try:
903
- ptr = ctypes.cast(value, ctypes.c_char_p)
920
+ raw = ctypes.string_at(address)
904
921
 
905
- # Only if we got a valid pointer with valid content
906
- 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
907
928
  try:
908
- py_string = ptr.value.decode('utf-8', errors='strict')
929
+ _lib.c2pa_string_free(value)
909
930
  except Exception:
910
- py_string = ""
911
- finally:
912
- # Only free if we have a valid pointer
913
- try:
914
- _lib.c2pa_string_free(value)
915
- except Exception:
916
- # Ignore clean up issues
917
- pass
918
- except (ctypes.ArgumentError, TypeError, ValueError):
931
+ # Ignore clean up issues
932
+ pass
933
+ except (ctypes.ArgumentError, TypeError, ValueError, OSError):
919
934
  # Invalid pointer type or value
920
935
  return ""
921
936
 
@@ -1003,8 +1018,7 @@ def _parse_operation_result_for_error(
1003
1018
  if check_error:
1004
1019
  error = _lib.c2pa_error()
1005
1020
  if error:
1006
- error_str = ctypes.cast(
1007
- error, ctypes.c_char_p).value.decode('utf-8')
1021
+ error_str = ctypes.string_at(error).decode('utf-8')
1008
1022
  _lib.c2pa_string_free(error)
1009
1023
  _raise_typed_c2pa_error(error_str)
1010
1024
  return None
@@ -1191,208 +1205,6 @@ def _get_mime_type_from_path(path: Union[str, Path]) -> str:
1191
1205
  return mime_type
1192
1206
 
1193
1207
 
1194
- def read_ingredient_file(
1195
- path: Union[str, Path], data_dir: Union[str, Path]) -> str:
1196
- """Read a file as C2PA ingredient (deprecated).
1197
- This creates the JSON string that would be used as the ingredient JSON.
1198
-
1199
- .. deprecated:: 0.11.0
1200
- This function is deprecated and will be removed in a future version.
1201
- To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
1202
- To add ingredients to a manifest,
1203
- use :meth:`c2pa.c2pa.Builder.add_ingredient` instead.
1204
-
1205
- Args:
1206
- path: Path to the file to read
1207
- data_dir: Directory to write binary resources to
1208
-
1209
- Returns:
1210
- The ingredient as a JSON string
1211
-
1212
- Raises:
1213
- C2paError: If there was an error reading the file
1214
- """
1215
- warnings.warn(
1216
- "The read_ingredient_file function is deprecated and will be "
1217
- "removed in a future version. Please use Reader(path).json() for "
1218
- "reading C2PA metadata instead, or "
1219
- "Builder.add_ingredient(json, format, stream) to add ingredients "
1220
- "to a manifest.",
1221
- DeprecationWarning,
1222
- stacklevel=2,
1223
- )
1224
-
1225
- _clear_error_state()
1226
-
1227
- container = _StringContainer()
1228
-
1229
- container._path_str = str(path).encode('utf-8')
1230
- container._data_dir_str = str(data_dir).encode('utf-8')
1231
-
1232
- result = _lib.c2pa_read_ingredient_file(
1233
- container._path_str, container._data_dir_str)
1234
-
1235
- _check_ffi_operation_result(
1236
- result, "Error reading ingredient file {}".format(path))
1237
-
1238
- return _convert_to_py_string(result)
1239
-
1240
-
1241
- def read_file(path: Union[str, Path],
1242
- data_dir: Union[str, Path]) -> str:
1243
- """Read a C2PA manifest from a file (deprecated).
1244
-
1245
- .. deprecated:: 0.10.0
1246
- This function is deprecated and will be removed in a future version.
1247
- To read C2PA metadata, use the :class:`c2pa.c2pa.Reader` class.
1248
-
1249
- Args:
1250
- path: Path to the file to read
1251
- data_dir: Directory to write binary resources to
1252
-
1253
- Returns:
1254
- The manifest as a JSON string
1255
-
1256
- Raises:
1257
- C2paError: If there was an error reading the file
1258
- """
1259
- warnings.warn(
1260
- "The read_file function is deprecated and will be removed in a "
1261
- "future version. Please use the Reader class for reading C2PA "
1262
- "metadata instead.",
1263
- DeprecationWarning,
1264
- stacklevel=2,
1265
- )
1266
-
1267
- _clear_error_state()
1268
-
1269
- container = _StringContainer()
1270
-
1271
- container._path_str = str(path).encode('utf-8')
1272
- container._data_dir_str = str(data_dir).encode('utf-8')
1273
-
1274
- result = _lib.c2pa_read_file(container._path_str, container._data_dir_str)
1275
- _check_ffi_operation_result(
1276
- result, "Error during read of manifest from file {}".format(path))
1277
-
1278
- return _convert_to_py_string(result)
1279
-
1280
-
1281
- @overload
1282
- def sign_file(
1283
- source_path: Union[str, Path],
1284
- dest_path: Union[str, Path],
1285
- manifest: str,
1286
- signer_info: C2paSignerInfo,
1287
- return_manifest_as_bytes: bool = False
1288
- ) -> Union[str, bytes]:
1289
- """Sign a file with a C2PA manifest using signer info.
1290
- """
1291
- ...
1292
-
1293
-
1294
- @overload
1295
- def sign_file(
1296
- source_path: Union[str, Path],
1297
- dest_path: Union[str, Path],
1298
- manifest: str,
1299
- signer: 'Signer',
1300
- return_manifest_as_bytes: bool = False
1301
- ) -> Union[str, bytes]:
1302
- """Sign a file with a C2PA manifest using a signer.
1303
- """
1304
- ...
1305
-
1306
-
1307
- def sign_file(
1308
- source_path: Union[str, Path],
1309
- dest_path: Union[str, Path],
1310
- manifest: str,
1311
- signer_or_info: Union[C2paSignerInfo, 'Signer'],
1312
- return_manifest_as_bytes: bool = False
1313
- ) -> Union[str, bytes]:
1314
- """Sign a file with a C2PA manifest (deprecated).
1315
- For now, this function is left here to provide a backwards-compatible API.
1316
-
1317
- .. deprecated:: 0.13.0
1318
- This function is deprecated and will be removed in a future version.
1319
- Use :meth:`Builder.sign` instead.
1320
-
1321
- Args:
1322
- source_path: Path to the source file. We will attempt
1323
- to guess the mimetype of the source file based on
1324
- the extension.
1325
- dest_path: Path to write the signed file to
1326
- manifest: The manifest JSON string
1327
- signer_or_info: Either a signer configuration or a signer object
1328
- return_manifest_as_bytes: If True, return manifest bytes instead
1329
- of JSON string
1330
-
1331
- Returns:
1332
- The signed manifest as a JSON string or bytes, depending
1333
- on return_manifest_as_bytes
1334
-
1335
- Raises:
1336
- C2paError: If there was an error signing the file
1337
- C2paError.Encoding: If any of the string inputs contain
1338
- invalid UTF-8 characters
1339
- C2paError.NotSupported: If the file type cannot be determined
1340
- """
1341
-
1342
- warnings.warn(
1343
- "The sign_file function is deprecated and will be removed in a "
1344
- "future version. Please use the Builder object and Builder.sign() "
1345
- "instead.",
1346
- DeprecationWarning,
1347
- stacklevel=2,
1348
- )
1349
-
1350
- _clear_error_state()
1351
-
1352
- try:
1353
- # Determine if we have a signer or signer info
1354
- if isinstance(signer_or_info, C2paSignerInfo):
1355
- signer = Signer.from_info(signer_or_info)
1356
- own_signer = True
1357
- else:
1358
- signer = signer_or_info
1359
- own_signer = False
1360
-
1361
- # Create a builder from the manifest
1362
- builder = Builder(manifest)
1363
-
1364
- manifest_bytes = builder.sign_file(
1365
- source_path,
1366
- dest_path,
1367
- signer
1368
- )
1369
-
1370
- if return_manifest_as_bytes:
1371
- return manifest_bytes
1372
- else:
1373
- # Read the signed manifest from the destination file
1374
- with Reader(dest_path) as reader:
1375
- return reader.json()
1376
-
1377
- except Exception as e:
1378
- # Clean up destination file if it exists and there was an error
1379
- if os.path.exists(dest_path):
1380
- try:
1381
- os.remove(dest_path)
1382
- except OSError:
1383
- logger.warning("Failed to remove destination file")
1384
- pass # Ignore cleanup errors
1385
-
1386
- # Re-raise the error
1387
- raise C2paError(f"Error signing file: {str(e)}") from e
1388
- finally:
1389
- # Ensure resources are cleaned up
1390
- if 'builder' in locals():
1391
- builder.close()
1392
- if 'signer' in locals() and own_signer:
1393
- signer.close()
1394
-
1395
-
1396
1208
  class ContextProvider(ABC):
1397
1209
  """Abstract base class for types that provide a C2PA context.
1398
1210
 
@@ -1827,10 +1639,21 @@ class Stream:
1827
1639
  readinto = getattr(stream, "readinto", None)
1828
1640
  if readinto is not None:
1829
1641
  # Most streams have readinto
1830
- buf = (ctypes.c_char * length).from_address(
1831
- ctypes.addressof(data.contents))
1832
- n = readinto(buf)
1833
- 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
1834
1657
 
1835
1658
  # Fallback for streams without readinto.
1836
1659
  buffer = stream.read(length)
@@ -3411,52 +3234,6 @@ class Builder(ManagedResource):
3411
3234
  Builder._ERROR_MESSAGES['ingredient_error'].format("Unknown error"),
3412
3235
  check=lambda r: r != 0)
3413
3236
 
3414
- def add_ingredient_from_file_path(
3415
- self,
3416
- ingredient_json: Union[str, dict],
3417
- format: str,
3418
- filepath: Union[str, Path]):
3419
- """Add an ingredient from a file path to the builder (deprecated).
3420
- This is a legacy method.
3421
-
3422
- .. deprecated:: 0.13.0
3423
- This method is deprecated and will be removed in a future version.
3424
- Use :meth:`add_ingredient` with a file stream instead.
3425
-
3426
- Args:
3427
- ingredient_json: The JSON ingredient definition
3428
- (either a JSON string or a dictionary)
3429
- format: The MIME type or extension of the ingredient
3430
- filepath: The path to the file containing the ingredient data
3431
- (can be a string or Path object)
3432
-
3433
- Raises:
3434
- C2paError: If there was an error adding the ingredient
3435
- C2paError.Encoding: If the ingredient JSON or format
3436
- contains invalid UTF-8 characters
3437
- FileNotFoundError: If the file at the specified path does not exist
3438
- """
3439
- warnings.warn(
3440
- "add_ingredient_from_file_path is deprecated and will "
3441
- "be removed in a future version. Use add_ingredient "
3442
- "with a file stream instead.",
3443
- DeprecationWarning,
3444
- stacklevel=2,
3445
- )
3446
-
3447
- try:
3448
- # Convert Path object to string if necessary
3449
- filepath_str = str(filepath)
3450
-
3451
- # Does the stream handling to use add_ingredient_from_stream
3452
- with open(filepath_str, 'rb') as file_stream:
3453
- self.add_ingredient_from_stream(
3454
- ingredient_json, format, file_stream)
3455
- except FileNotFoundError as e:
3456
- raise C2paError.FileNotFound(f"File not found: {filepath}") from e
3457
- except Exception as e:
3458
- raise C2paError.Other(f"Could not add ingredient: {e}") from e
3459
-
3460
3237
  def add_action(self, action_json: Union[str, dict]) -> None:
3461
3238
  """Add an action to the builder, that will be placed
3462
3239
  in the actions assertion array in the generated manifest.
@@ -3991,9 +3768,6 @@ __all__ = [
3991
3768
  'Builder',
3992
3769
  'Signer',
3993
3770
  'load_settings',
3994
- 'read_file',
3995
- 'read_ingredient_file',
3996
- 'sign_file',
3997
3771
  'format_embeddable',
3998
3772
  'version',
3999
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.14
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()
@@ -4787,9 +4787,6 @@ class TestStream(unittest.TestCase):
4787
4787
  class TestLegacyAPI(unittest.TestCase):
4788
4788
  def setUp(self):
4789
4789
  # Filter specific deprecation warnings for legacy API tests
4790
- warnings.filterwarnings("ignore", message="The read_file function is deprecated")
4791
- warnings.filterwarnings("ignore", message="The sign_file function is deprecated")
4792
- warnings.filterwarnings("ignore", message="The read_ingredient_file function is deprecated")
4793
4790
  warnings.filterwarnings("ignore", message="The create_signer function is deprecated")
4794
4791
  warnings.filterwarnings("ignore", message="The create_signer_from_info function is deprecated")
4795
4792
  warnings.filterwarnings("ignore", message="load_settings\\(\\) is deprecated")
@@ -4863,489 +4860,6 @@ class TestLegacyAPI(unittest.TestCase):
4863
4860
  if os.path.exists(self.temp_data_dir):
4864
4861
  shutil.rmtree(self.temp_data_dir)
4865
4862
 
4866
- def test_read_ingredient_file(self):
4867
- """Test reading a C2PA ingredient from a file."""
4868
- # Test reading ingredient from file with data_dir
4869
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4870
- os.makedirs(temp_data_dir, exist_ok=True)
4871
-
4872
- ingredient_json_with_dir = read_ingredient_file(self.testPath, temp_data_dir)
4873
-
4874
- # Verify some fields
4875
- ingredient_data = json.loads(ingredient_json_with_dir)
4876
- self.assertEqual(ingredient_data["title"], DEFAULT_TEST_FILE_NAME)
4877
- self.assertEqual(ingredient_data["format"], "image/jpeg")
4878
- self.assertIn("thumbnail", ingredient_data)
4879
-
4880
- def test_read_ingredient_file_who_has_no_manifest(self):
4881
- """Test reading a C2PA ingredient from a file."""
4882
- # Test reading ingredient from file with data_dir
4883
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4884
- os.makedirs(temp_data_dir, exist_ok=True)
4885
-
4886
- # Load settings first, before they need to be used
4887
- load_settings('{"builder": { "thumbnail": {"enabled": false}}}')
4888
-
4889
- ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
4890
-
4891
- # Verify some fields
4892
- ingredient_data = json.loads(ingredient_json_with_dir)
4893
- self.assertEqual(ingredient_data["title"], INGREDIENT_TEST_FILE_NAME)
4894
- self.assertEqual(ingredient_data["format"], "image/jpeg")
4895
- self.assertNotIn("thumbnail", ingredient_data)
4896
-
4897
- # Reset setting
4898
- load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
4899
-
4900
- def test_compare_read_ingredient_file_with_builder_added_ingredient(self):
4901
- """Test reading a C2PA ingredient from a file."""
4902
- # Test reading ingredient from file with data_dir
4903
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4904
- os.makedirs(temp_data_dir, exist_ok=True)
4905
-
4906
- ingredient_json_with_dir = read_ingredient_file(self.testPath2, temp_data_dir)
4907
-
4908
- # Ingredient fields from read_ingredient_file
4909
- ingredient_data = json.loads(ingredient_json_with_dir)
4910
-
4911
- # Compare with ingredient added by Builder
4912
- builder = Builder.from_json(self.manifestDefinition)
4913
- # Only the title is needed (filename), since title not extracted or guessed from filename
4914
- ingredient_json = '{ "title" : "A.jpg" }'
4915
- with open(self.testPath2, 'rb') as f:
4916
- builder.add_ingredient(ingredient_json, "image/jpeg", f)
4917
-
4918
- with open(self.testPath2, "rb") as file:
4919
- output = io.BytesIO(bytearray())
4920
- builder.sign(self.signer, "image/jpeg", file, output)
4921
- output.seek(0)
4922
-
4923
- # Get ingredient fields from signed manifest
4924
- reader = Reader("image/jpeg", output)
4925
- json_data = reader.json()
4926
- manifest_data = json.loads(json_data)
4927
- active_manifest_id = manifest_data["active_manifest"]
4928
- active_manifest = manifest_data["manifests"][active_manifest_id]
4929
- only_ingredient = active_manifest["ingredients"][0]
4930
-
4931
- self.assertEqual(ingredient_data["title"], only_ingredient["title"])
4932
- self.assertEqual(ingredient_data["format"], only_ingredient["format"])
4933
- self.assertEqual(ingredient_data["document_id"], only_ingredient["document_id"])
4934
- self.assertEqual(ingredient_data["instance_id"], only_ingredient["instance_id"])
4935
- self.assertEqual(ingredient_data["relationship"], only_ingredient["relationship"])
4936
-
4937
- def test_read_file(self):
4938
- """Test reading a C2PA ingredient from a file."""
4939
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4940
- os.makedirs(temp_data_dir, exist_ok=True)
4941
-
4942
- # self.testPath has C2PA metadata to read
4943
- file_json_with_dir = read_file(self.testPath, temp_data_dir)
4944
-
4945
- # Parse the JSON and verify specific fields
4946
- file_data = json.loads(file_json_with_dir)
4947
- expected_manifest_id = "contentauth:urn:uuid:c85a2b90-f1a0-4aa4-b17f-f938b475804e"
4948
-
4949
- # Verify some fields
4950
- self.assertEqual(file_data["active_manifest"], expected_manifest_id)
4951
- self.assertIn("manifests", file_data)
4952
- self.assertIn(expected_manifest_id, file_data["manifests"])
4953
-
4954
- def test_sign_file(self):
4955
- """Test signing a file with C2PA manifest."""
4956
- # Set up test paths
4957
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
4958
- os.makedirs(temp_data_dir, exist_ok=True)
4959
- output_path = os.path.join(temp_data_dir, "signed_output.jpg")
4960
-
4961
- # Load test certificates and key
4962
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
4963
- certs = cert_file.read()
4964
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
4965
- key = key_file.read()
4966
-
4967
- # Create signer info
4968
- signer_info = C2paSignerInfo(
4969
- alg=b"es256",
4970
- sign_cert=certs,
4971
- private_key=key,
4972
- ta_url=b"http://timestamp.digicert.com"
4973
- )
4974
-
4975
- # Create a simple manifest
4976
- manifest = {
4977
- "claim_generator": "python_internals_test",
4978
- "claim_generator_info": [{
4979
- "name": "python_internals_test",
4980
- "version": "0.0.1",
4981
- }],
4982
- # Claim version has become mandatory for signing v1 claims
4983
- "claim_version": 1,
4984
- "format": "image/jpeg",
4985
- "title": "Python Test Signed Image",
4986
- "ingredients": [],
4987
- "assertions": [
4988
- {
4989
- "label": "c2pa.actions",
4990
- "data": {
4991
- "actions": [
4992
- {
4993
- "action": "c2pa.opened"
4994
- }
4995
- ]
4996
- }
4997
- }
4998
- ]
4999
- }
5000
-
5001
- # Convert manifest to JSON string
5002
- manifest_json = json.dumps(manifest)
5003
-
5004
- try:
5005
- sign_file(
5006
- self.testPath,
5007
- output_path,
5008
- manifest_json,
5009
- signer_info
5010
- )
5011
-
5012
- finally:
5013
- # Clean up
5014
- if os.path.exists(output_path):
5015
- os.remove(output_path)
5016
-
5017
- def test_sign_file_does_not_exist_errors(self):
5018
- """Test signing a file with C2PA manifest."""
5019
- # Set up test paths
5020
- temp_data_dir = os.path.join(self.data_dir, "temp_data")
5021
- os.makedirs(temp_data_dir, exist_ok=True)
5022
- output_path = os.path.join(temp_data_dir, "signed_output.jpg")
5023
-
5024
- # Load test certificates and key
5025
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
5026
- certs = cert_file.read()
5027
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
5028
- key = key_file.read()
5029
-
5030
- # Create signer info
5031
- signer_info = C2paSignerInfo(
5032
- alg=b"es256",
5033
- sign_cert=certs,
5034
- private_key=key,
5035
- ta_url=b"http://timestamp.digicert.com"
5036
- )
5037
-
5038
- # Create a simple manifest
5039
- manifest = {
5040
- "claim_generator": "python_internals_test",
5041
- "claim_generator_info": [{
5042
- "name": "python_internals_test",
5043
- "version": "0.0.1",
5044
- }],
5045
- # Claim version has become mandatory for signing v1 claims
5046
- "claim_version": 1,
5047
- "format": "image/jpeg",
5048
- "title": "Python Test Signed Image",
5049
- "ingredients": [],
5050
- "assertions": [
5051
- {
5052
- "label": "c2pa.actions",
5053
- "data": {
5054
- "actions": [
5055
- {
5056
- "action": "c2pa.opened"
5057
- }
5058
- ]
5059
- }
5060
- }
5061
- ]
5062
- }
5063
-
5064
- # Convert manifest to JSON string
5065
- manifest_json = json.dumps(manifest)
5066
-
5067
- try:
5068
- with self.assertRaises(Error):
5069
- sign_file(
5070
- "this-file-does-not-exist",
5071
- output_path,
5072
- manifest_json,
5073
- signer_info
5074
- )
5075
-
5076
- finally:
5077
- # Clean up
5078
- if os.path.exists(output_path):
5079
- os.remove(output_path)
5080
-
5081
- def test_builder_sign_with_ingredient_from_file(self):
5082
- """Test Builder class operations with an ingredient added from file path."""
5083
-
5084
- builder = Builder.from_json(self.manifestDefinition)
5085
-
5086
- # Test adding ingredient from file path
5087
- ingredient_json = '{"title": "Test Ingredient From File"}'
5088
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5089
- with warnings.catch_warnings():
5090
- warnings.simplefilter("ignore", DeprecationWarning)
5091
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath3)
5092
-
5093
- with open(self.testPath2, "rb") as file:
5094
- output = io.BytesIO(bytearray())
5095
- builder.sign(self.signer, "image/jpeg", file, output)
5096
- output.seek(0)
5097
- reader = Reader("image/jpeg", output)
5098
- json_data = reader.json()
5099
- manifest_data = json.loads(json_data)
5100
-
5101
- # Verify active manifest exists
5102
- self.assertIn("active_manifest", manifest_data)
5103
- active_manifest_id = manifest_data["active_manifest"]
5104
-
5105
- # Verify active manifest object exists
5106
- self.assertIn("manifests", manifest_data)
5107
- self.assertIn(active_manifest_id, manifest_data["manifests"])
5108
- active_manifest = manifest_data["manifests"][active_manifest_id]
5109
-
5110
- # Verify ingredients array exists in active manifest
5111
- self.assertIn("ingredients", active_manifest)
5112
- self.assertIsInstance(active_manifest["ingredients"], list)
5113
- self.assertTrue(len(active_manifest["ingredients"]) > 0)
5114
-
5115
- # Verify the first ingredient's title matches what we set
5116
- first_ingredient = active_manifest["ingredients"][0]
5117
- self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
5118
-
5119
- builder.close()
5120
-
5121
- def test_builder_sign_with_ingredient_dict_from_file(self):
5122
- """Test Builder class operations with an ingredient added from file path using a dictionary."""
5123
-
5124
- builder = Builder.from_json(self.manifestDefinition)
5125
-
5126
- # Test adding ingredient from file path with a dictionary
5127
- ingredient_dict = {"title": "Test Ingredient From File"}
5128
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5129
- with warnings.catch_warnings():
5130
- warnings.simplefilter("ignore", DeprecationWarning)
5131
- builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3)
5132
-
5133
- with open(self.testPath2, "rb") as file:
5134
- output = io.BytesIO(bytearray())
5135
- builder.sign(self.signer, "image/jpeg", file, output)
5136
- output.seek(0)
5137
- reader = Reader("image/jpeg", output)
5138
- json_data = reader.json()
5139
- manifest_data = json.loads(json_data)
5140
-
5141
- # Verify active manifest exists
5142
- self.assertIn("active_manifest", manifest_data)
5143
- active_manifest_id = manifest_data["active_manifest"]
5144
-
5145
- # Verify active manifest object exists
5146
- self.assertIn("manifests", manifest_data)
5147
- self.assertIn(active_manifest_id, manifest_data["manifests"])
5148
- active_manifest = manifest_data["manifests"][active_manifest_id]
5149
-
5150
- # Verify ingredients array exists in active manifest
5151
- self.assertIn("ingredients", active_manifest)
5152
- self.assertIsInstance(active_manifest["ingredients"], list)
5153
- self.assertTrue(len(active_manifest["ingredients"]) > 0)
5154
-
5155
- # Verify the first ingredient's title matches what we set
5156
- first_ingredient = active_manifest["ingredients"][0]
5157
- self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
5158
-
5159
- builder.close()
5160
-
5161
- def test_builder_add_ingredient_from_file_path(self):
5162
- """Test Builder class add_ingredient_from_file_path method."""
5163
-
5164
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5165
- with warnings.catch_warnings():
5166
- warnings.simplefilter("ignore", DeprecationWarning)
5167
-
5168
- builder = Builder.from_json(self.manifestDefinition)
5169
-
5170
- # Test adding ingredient from file path
5171
- ingredient_json = '{"test": "ingredient_from_file_path"}'
5172
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", self.testPath)
5173
-
5174
- builder.close()
5175
-
5176
- def test_builder_add_ingredient_from_file_path_not_found(self):
5177
- """Test Builder class add_ingredient_from_file_path method."""
5178
-
5179
- # Suppress the specific deprecation warning for this test, as this is a legacy method
5180
- with warnings.catch_warnings():
5181
- warnings.simplefilter("ignore", DeprecationWarning)
5182
-
5183
- builder = Builder.from_json(self.manifestDefinition)
5184
-
5185
- # Test adding ingredient from file path
5186
- ingredient_json = '{"test": "ingredient_from_file_path"}'
5187
-
5188
- with self.assertRaises(Error.FileNotFound):
5189
- builder.add_ingredient_from_file_path(ingredient_json, "image/jpeg", "this-file-path-does-not-exist")
5190
-
5191
- def test_sign_file_using_callback_signer_overloads(self):
5192
- """Test signing a file using the sign_file function with a Signer object."""
5193
- # Create a temporary directory for the test
5194
- temp_dir = tempfile.mkdtemp()
5195
-
5196
- try:
5197
- # Create a temporary output file path
5198
- output_path = os.path.join(temp_dir, "signed_output_callback.jpg")
5199
-
5200
- # Create signer with callback
5201
- signer = Signer.from_callback(
5202
- callback=self.callback_signer_es256,
5203
- alg=SigningAlg.ES256,
5204
- certs=self.certs.decode('utf-8'),
5205
- tsa_url="http://timestamp.digicert.com"
5206
- )
5207
-
5208
- # Overload that returns a JSON string
5209
- result_json = sign_file(
5210
- self.testPath,
5211
- output_path,
5212
- self.manifestDefinition,
5213
- signer,
5214
- False
5215
- )
5216
-
5217
- # Verify the output file was created
5218
- self.assertTrue(os.path.exists(output_path))
5219
-
5220
- # Verify the result is JSON
5221
- self.assertIsInstance(result_json, str)
5222
- self.assertGreater(len(result_json), 0)
5223
-
5224
- manifest_data = json.loads(result_json)
5225
- self.assertIn("manifests", manifest_data)
5226
- self.assertIn("active_manifest", manifest_data)
5227
-
5228
- output_path_bytes = os.path.join(temp_dir, "signed_output_callback_bytes.jpg")
5229
- # Overload that returns bytes
5230
- result_bytes = sign_file(
5231
- self.testPath,
5232
- output_path_bytes,
5233
- self.manifestDefinition,
5234
- signer,
5235
- True
5236
- )
5237
-
5238
- # Verify the output file was created
5239
- self.assertTrue(os.path.exists(output_path_bytes))
5240
-
5241
- # Verify the result is bytes
5242
- self.assertIsInstance(result_bytes, bytes)
5243
- self.assertGreater(len(result_bytes), 0)
5244
-
5245
- # Read the signed file and verify the manifest contains expected content
5246
- with open(output_path, "rb") as file:
5247
- reader = Reader("image/jpeg", file)
5248
- file_manifest_json = reader.json()
5249
- self.assertIn("Python Test", file_manifest_json)
5250
-
5251
- finally:
5252
- shutil.rmtree(temp_dir)
5253
-
5254
- def test_sign_file_overloads(self):
5255
- """Test that the overloaded sign_file function works with both parameter types."""
5256
- # Create a temporary directory for the test
5257
- temp_dir = tempfile.mkdtemp()
5258
- try:
5259
- # Test with C2paSignerInfo
5260
- output_path_1 = os.path.join(temp_dir, "signed_output_1.jpg")
5261
-
5262
- # Load test certificates and key
5263
- with open(os.path.join(self.data_dir, "es256_certs.pem"), "rb") as cert_file:
5264
- certs = cert_file.read()
5265
- with open(os.path.join(self.data_dir, "es256_private.key"), "rb") as key_file:
5266
- key = key_file.read()
5267
-
5268
- # Create signer info
5269
- signer_info = C2paSignerInfo(
5270
- alg=b"es256",
5271
- sign_cert=certs,
5272
- private_key=key,
5273
- ta_url=b"http://timestamp.digicert.com"
5274
- )
5275
-
5276
- # Test with C2paSignerInfo parameter - JSON return
5277
- result_1 = sign_file(
5278
- self.testPath,
5279
- output_path_1,
5280
- self.manifestDefinition,
5281
- signer_info,
5282
- False
5283
- )
5284
-
5285
- self.assertIsInstance(result_1, str)
5286
- self.assertTrue(os.path.exists(output_path_1))
5287
-
5288
- # Test with C2paSignerInfo parameter - bytes return
5289
- output_path_1_bytes = os.path.join(temp_dir, "signed_output_1_bytes.jpg")
5290
- result_1_bytes = sign_file(
5291
- self.testPath,
5292
- output_path_1_bytes,
5293
- self.manifestDefinition,
5294
- signer_info,
5295
- True
5296
- )
5297
-
5298
- self.assertIsInstance(result_1_bytes, bytes)
5299
- self.assertTrue(os.path.exists(output_path_1_bytes))
5300
-
5301
- # Test with Signer object
5302
- output_path_2 = os.path.join(temp_dir, "signed_output_2.jpg")
5303
-
5304
- # Create a signer from the signer info
5305
- signer = Signer.from_info(signer_info)
5306
-
5307
- # Test with Signer parameter - JSON return
5308
- result_2 = sign_file(
5309
- self.testPath,
5310
- output_path_2,
5311
- self.manifestDefinition,
5312
- signer,
5313
- False
5314
- )
5315
-
5316
- self.assertIsInstance(result_2, str)
5317
- self.assertTrue(os.path.exists(output_path_2))
5318
-
5319
- # Test with Signer parameter - bytes return
5320
- output_path_2_bytes = os.path.join(temp_dir, "signed_output_2_bytes.jpg")
5321
- result_2_bytes = sign_file(
5322
- self.testPath,
5323
- output_path_2_bytes,
5324
- self.manifestDefinition,
5325
- signer,
5326
- True
5327
- )
5328
-
5329
- self.assertIsInstance(result_2_bytes, bytes)
5330
- self.assertTrue(os.path.exists(output_path_2_bytes))
5331
-
5332
- # Both JSON results should be similar (same manifest structure)
5333
- manifest_1 = json.loads(result_1)
5334
- manifest_2 = json.loads(result_2)
5335
-
5336
- self.assertIn("manifests", manifest_1)
5337
- self.assertIn("manifests", manifest_2)
5338
- self.assertIn("active_manifest", manifest_1)
5339
- self.assertIn("active_manifest", manifest_2)
5340
-
5341
- # Both bytes results should be non-empty
5342
- self.assertGreater(len(result_1_bytes), 0)
5343
- self.assertGreater(len(result_2_bytes), 0)
5344
-
5345
- finally:
5346
- # Clean up the temporary directory
5347
- shutil.rmtree(temp_dir)
5348
-
5349
4863
  def test_sign_file_callback_signer_reports_error(self):
5350
4864
  """Test signing a file using the sign_file method with a callback that reports an error."""
5351
4865
 
File without changes
File without changes
File without changes
File without changes