c2pa-python 0.16.0__tar.gz → 0.18.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.
- {c2pa_python-0.16.0/src/c2pa_python.egg-info → c2pa_python-0.18.0}/PKG-INFO +1 -1
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/pyproject.toml +1 -1
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa/c2pa.py +455 -160
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa/lib.py +11 -18
- {c2pa_python-0.16.0 → c2pa_python-0.18.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/tests/test_unit_tests.py +26 -3
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/LICENSE-APACHE +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/LICENSE-MIT +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/MANIFEST.in +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/README.md +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/requirements.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/setup.cfg +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/setup.py +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa/build.py +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
- {c2pa_python-0.16.0 → c2pa_python-0.18.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.
|
|
3
|
+
Version: 0.18.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.
|
|
7
|
+
version = "0.18.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" }
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import ctypes
|
|
15
15
|
import enum
|
|
16
16
|
import json
|
|
17
|
+
import logging
|
|
17
18
|
import sys
|
|
18
19
|
import os
|
|
19
20
|
import warnings
|
|
@@ -23,6 +24,10 @@ import io
|
|
|
23
24
|
from .lib import dynamically_load_library
|
|
24
25
|
import mimetypes
|
|
25
26
|
|
|
27
|
+
# Create a module-specific logger
|
|
28
|
+
logger = logging.getLogger("c2pa")
|
|
29
|
+
logger.addHandler(logging.NullHandler())
|
|
30
|
+
|
|
26
31
|
# Define required function names
|
|
27
32
|
_REQUIRED_FUNCTIONS = [
|
|
28
33
|
'c2pa_version',
|
|
@@ -255,6 +260,12 @@ class C2paSignerInfo(ctypes.Structure):
|
|
|
255
260
|
private_key: The private key as a string
|
|
256
261
|
ta_url: The timestamp authority URL as bytes
|
|
257
262
|
"""
|
|
263
|
+
|
|
264
|
+
if sign_cert is None:
|
|
265
|
+
raise ValueError("sign_cert must be set")
|
|
266
|
+
if private_key is None:
|
|
267
|
+
raise ValueError("private_key must be set")
|
|
268
|
+
|
|
258
269
|
# Handle alg parameter: can be C2paSigningAlg enum
|
|
259
270
|
# or string (or bytes), convert as needed
|
|
260
271
|
if isinstance(alg, C2paSigningAlg):
|
|
@@ -545,19 +556,31 @@ def _convert_to_py_string(value) -> str:
|
|
|
545
556
|
return ""
|
|
546
557
|
|
|
547
558
|
py_string = ""
|
|
548
|
-
ptr = ctypes.cast(value, ctypes.c_char_p)
|
|
549
559
|
|
|
550
|
-
#
|
|
551
|
-
if
|
|
552
|
-
|
|
553
|
-
py_string = ptr.value.decode('utf-8', errors='replace')
|
|
554
|
-
except Exception:
|
|
555
|
-
py_string = ""
|
|
560
|
+
# Validate pointer before casting and freeing
|
|
561
|
+
if not isinstance(value, (int, ctypes.c_void_p)) or value == 0:
|
|
562
|
+
return ""
|
|
556
563
|
|
|
557
|
-
|
|
558
|
-
|
|
564
|
+
try:
|
|
565
|
+
ptr = ctypes.cast(value, ctypes.c_char_p)
|
|
566
|
+
|
|
567
|
+
# Only if we got a valid pointer with valid content
|
|
568
|
+
if ptr and ptr.value is not None:
|
|
569
|
+
try:
|
|
570
|
+
py_string = ptr.value.decode('utf-8', errors='replace')
|
|
571
|
+
except Exception:
|
|
572
|
+
py_string = ""
|
|
573
|
+
finally:
|
|
574
|
+
# Only free if we have a valid pointer
|
|
575
|
+
try:
|
|
576
|
+
_lib.c2pa_string_free(value)
|
|
577
|
+
except Exception:
|
|
578
|
+
# Ignore clean up issues
|
|
579
|
+
pass
|
|
580
|
+
except (ctypes.ArgumentError, TypeError, ValueError):
|
|
581
|
+
# Invalid pointer type or value
|
|
582
|
+
return ""
|
|
559
583
|
|
|
560
|
-
# In case of invalid pointer, no free (avoids double-free)
|
|
561
584
|
return py_string
|
|
562
585
|
|
|
563
586
|
|
|
@@ -617,7 +640,7 @@ def sdk_version() -> str:
|
|
|
617
640
|
Returns the underlying c2pa-rs/c2pa-c-ffi version string
|
|
618
641
|
"""
|
|
619
642
|
vstr = version()
|
|
620
|
-
# Example: "c2pa-c/0.
|
|
643
|
+
# Example: "c2pa-c/0.60.1 c2pa-rs/0.60.1"
|
|
621
644
|
for part in vstr.split():
|
|
622
645
|
if part.startswith("c2pa-rs/"):
|
|
623
646
|
return part.split("/", 1)[1]
|
|
@@ -654,6 +677,33 @@ def load_settings(settings: str, format: str = "json") -> None:
|
|
|
654
677
|
return result
|
|
655
678
|
|
|
656
679
|
|
|
680
|
+
def _get_mime_type_from_path(path: Union[str, Path]) -> str:
|
|
681
|
+
"""Attempt to guess the MIME type from a file path (with extension).
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
path: File path as string or Path object
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
MIME type string
|
|
688
|
+
|
|
689
|
+
Raises:
|
|
690
|
+
C2paError.NotSupported: If MIME type cannot be determined
|
|
691
|
+
"""
|
|
692
|
+
path_obj = Path(path)
|
|
693
|
+
file_extension = path_obj.suffix.lower() if path_obj.suffix else ""
|
|
694
|
+
|
|
695
|
+
if file_extension == ".dng":
|
|
696
|
+
# mimetypes guesses the wrong type for dng,
|
|
697
|
+
# so we bypass it and set the correct type
|
|
698
|
+
return "image/dng"
|
|
699
|
+
else:
|
|
700
|
+
mime_type = mimetypes.guess_type(str(path))[0]
|
|
701
|
+
if not mime_type:
|
|
702
|
+
raise C2paError.NotSupported(
|
|
703
|
+
f"Could not determine MIME type for file: {path}")
|
|
704
|
+
return mime_type
|
|
705
|
+
|
|
706
|
+
|
|
657
707
|
def read_ingredient_file(
|
|
658
708
|
path: Union[str, Path], data_dir: Union[str, Path]) -> str:
|
|
659
709
|
"""Read a file as C2PA ingredient.
|
|
@@ -803,7 +853,9 @@ def sign_file(
|
|
|
803
853
|
Use :meth:`Builder.sign` instead.
|
|
804
854
|
|
|
805
855
|
Args:
|
|
806
|
-
source_path: Path to the source file
|
|
856
|
+
source_path: Path to the source file. We will attempt
|
|
857
|
+
to guess the mimetype of the source file based on
|
|
858
|
+
the extension.
|
|
807
859
|
dest_path: Path to write the signed file to
|
|
808
860
|
manifest: The manifest JSON string
|
|
809
861
|
signer_or_info: Either a signer configuration or a signer object
|
|
@@ -860,6 +912,7 @@ def sign_file(
|
|
|
860
912
|
try:
|
|
861
913
|
os.remove(dest_path)
|
|
862
914
|
except OSError:
|
|
915
|
+
logger.warning("Failed to remove destination file")
|
|
863
916
|
pass # Ignore cleanup errors
|
|
864
917
|
|
|
865
918
|
# Re-raise the error
|
|
@@ -1097,9 +1150,30 @@ class Stream:
|
|
|
1097
1150
|
self.close()
|
|
1098
1151
|
|
|
1099
1152
|
def __del__(self):
|
|
1100
|
-
"""Ensure resources are cleaned up if close()
|
|
1101
|
-
|
|
1102
|
-
|
|
1153
|
+
"""Ensure resources are cleaned up if close()wasn't called.
|
|
1154
|
+
|
|
1155
|
+
This destructor only cleans up if the object
|
|
1156
|
+
hasn't been explicitly closed.
|
|
1157
|
+
"""
|
|
1158
|
+
try:
|
|
1159
|
+
# Only cleanup if not already closed and we have a valid stream
|
|
1160
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
1161
|
+
if hasattr(self, '_stream') and self._stream:
|
|
1162
|
+
# Use internal cleanup to avoid calling close() which could
|
|
1163
|
+
# cause issues
|
|
1164
|
+
try:
|
|
1165
|
+
_lib.c2pa_release_stream(self._stream)
|
|
1166
|
+
except Exception:
|
|
1167
|
+
# Destructors shouldn't raise exceptions
|
|
1168
|
+
logger.error("Failed to release Stream")
|
|
1169
|
+
pass
|
|
1170
|
+
finally:
|
|
1171
|
+
self._stream = None
|
|
1172
|
+
self._closed = True
|
|
1173
|
+
self._initialized = False
|
|
1174
|
+
except Exception:
|
|
1175
|
+
# Destructors must not raise exceptions
|
|
1176
|
+
pass
|
|
1103
1177
|
|
|
1104
1178
|
def close(self):
|
|
1105
1179
|
"""Release the stream resources.
|
|
@@ -1121,9 +1195,9 @@ class Stream:
|
|
|
1121
1195
|
try:
|
|
1122
1196
|
_lib.c2pa_release_stream(self._stream)
|
|
1123
1197
|
except Exception as e:
|
|
1124
|
-
|
|
1198
|
+
logger.error(
|
|
1125
1199
|
Stream._ERROR_MESSAGES['stream_error'].format(
|
|
1126
|
-
str(e))
|
|
1200
|
+
str(e)))
|
|
1127
1201
|
finally:
|
|
1128
1202
|
self._stream = None
|
|
1129
1203
|
|
|
@@ -1133,14 +1207,14 @@ class Stream:
|
|
|
1133
1207
|
try:
|
|
1134
1208
|
setattr(self, attr, None)
|
|
1135
1209
|
except Exception as e:
|
|
1136
|
-
|
|
1210
|
+
logger.error(
|
|
1137
1211
|
Stream._ERROR_MESSAGES['callback_error'].format(
|
|
1138
|
-
attr, str(e))
|
|
1212
|
+
attr, str(e)))
|
|
1139
1213
|
|
|
1140
1214
|
except Exception as e:
|
|
1141
|
-
|
|
1215
|
+
logger.error(
|
|
1142
1216
|
Stream._ERROR_MESSAGES['cleanup_error'].format(
|
|
1143
|
-
str(e))
|
|
1217
|
+
str(e)))
|
|
1144
1218
|
finally:
|
|
1145
1219
|
self._closed = True
|
|
1146
1220
|
self._initialized = False
|
|
@@ -1184,26 +1258,71 @@ class Reader:
|
|
|
1184
1258
|
'stream_error': "Error cleaning up stream: {}",
|
|
1185
1259
|
'file_error': "Error cleaning up file: {}",
|
|
1186
1260
|
'reader_cleanup_error': "Error cleaning up reader: {}",
|
|
1187
|
-
'encoding_error': "Invalid UTF-8 characters in input: {}"
|
|
1261
|
+
'encoding_error': "Invalid UTF-8 characters in input: {}",
|
|
1262
|
+
'closed_error': "Reader is closed"
|
|
1188
1263
|
}
|
|
1189
1264
|
|
|
1190
1265
|
@classmethod
|
|
1191
1266
|
def get_supported_mime_types(cls) -> list[str]:
|
|
1267
|
+
"""Get the list of supported MIME types for the Reader.
|
|
1268
|
+
This method retrieves supported MIME types from the native library.
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
List of supported MIME type strings
|
|
1272
|
+
|
|
1273
|
+
Raises:
|
|
1274
|
+
C2paError: If there was an error retrieving the MIME types
|
|
1275
|
+
"""
|
|
1192
1276
|
if cls._supported_mime_types_cache is not None:
|
|
1193
1277
|
return cls._supported_mime_types_cache
|
|
1194
1278
|
|
|
1195
1279
|
count = ctypes.c_size_t()
|
|
1196
1280
|
arr = _lib.c2pa_reader_supported_mime_types(ctypes.byref(count))
|
|
1197
1281
|
|
|
1282
|
+
# Validate the returned array pointer
|
|
1283
|
+
if not arr:
|
|
1284
|
+
# If no array returned, check for errors
|
|
1285
|
+
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
1286
|
+
if error:
|
|
1287
|
+
raise C2paError(f"Failed to get supported MIME types: {error}")
|
|
1288
|
+
# Return empty list if no error but no array
|
|
1289
|
+
return []
|
|
1290
|
+
|
|
1291
|
+
# Validate count value
|
|
1292
|
+
if count.value <= 0:
|
|
1293
|
+
# Free the array even if count is invalid
|
|
1294
|
+
try:
|
|
1295
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
1296
|
+
except Exception:
|
|
1297
|
+
pass
|
|
1298
|
+
return []
|
|
1299
|
+
|
|
1198
1300
|
try:
|
|
1199
|
-
|
|
1200
|
-
|
|
1301
|
+
result = []
|
|
1302
|
+
for i in range(count.value):
|
|
1303
|
+
try:
|
|
1304
|
+
# Validate each array element before accessing
|
|
1305
|
+
if arr[i] is None:
|
|
1306
|
+
continue
|
|
1307
|
+
|
|
1308
|
+
mime_type = arr[i].decode("utf-8", errors='replace')
|
|
1309
|
+
if mime_type:
|
|
1310
|
+
result.append(mime_type)
|
|
1311
|
+
except Exception:
|
|
1312
|
+
# Ignore cleanup errors
|
|
1313
|
+
continue
|
|
1201
1314
|
finally:
|
|
1202
|
-
#
|
|
1203
|
-
|
|
1204
|
-
|
|
1315
|
+
# Always free the native memory, even if string extraction fails
|
|
1316
|
+
try:
|
|
1317
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
1318
|
+
except Exception:
|
|
1319
|
+
# Ignore cleanup errors
|
|
1320
|
+
pass
|
|
1321
|
+
|
|
1322
|
+
# Cache the result
|
|
1323
|
+
if result:
|
|
1324
|
+
cls._supported_mime_types_cache = result
|
|
1205
1325
|
|
|
1206
|
-
cls._supported_mime_types_cache = result
|
|
1207
1326
|
return cls._supported_mime_types_cache
|
|
1208
1327
|
|
|
1209
1328
|
def __init__(self,
|
|
@@ -1224,15 +1343,20 @@ class Reader:
|
|
|
1224
1343
|
contain invalid UTF-8 characters
|
|
1225
1344
|
"""
|
|
1226
1345
|
|
|
1346
|
+
self._closed = False
|
|
1347
|
+
|
|
1227
1348
|
self._reader = None
|
|
1228
1349
|
self._own_stream = None
|
|
1229
1350
|
|
|
1351
|
+
# This is used to keep track of a file
|
|
1352
|
+
# we may have opened ourselves, and that we need to close later
|
|
1353
|
+
self._backing_file = None
|
|
1354
|
+
|
|
1230
1355
|
if stream is None:
|
|
1231
1356
|
# If we don't get a stream as param:
|
|
1232
1357
|
# Create a stream from the file path in format_or_path
|
|
1233
1358
|
path = str(format_or_path)
|
|
1234
|
-
mime_type =
|
|
1235
|
-
path)[0]
|
|
1359
|
+
mime_type = _get_mime_type_from_path(path)
|
|
1236
1360
|
|
|
1237
1361
|
if not mime_type:
|
|
1238
1362
|
raise C2paError.NotSupported(
|
|
@@ -1243,7 +1367,7 @@ class Reader:
|
|
|
1243
1367
|
f"Reader does not support {mime_type}")
|
|
1244
1368
|
|
|
1245
1369
|
try:
|
|
1246
|
-
|
|
1370
|
+
mime_type_str = mime_type.encode('utf-8')
|
|
1247
1371
|
except UnicodeError as e:
|
|
1248
1372
|
raise C2paError.Encoding(
|
|
1249
1373
|
Reader._ERROR_MESSAGES['encoding_error'].format(
|
|
@@ -1255,7 +1379,7 @@ class Reader:
|
|
|
1255
1379
|
self._own_stream = Stream(file)
|
|
1256
1380
|
|
|
1257
1381
|
self._reader = _lib.c2pa_reader_from_stream(
|
|
1258
|
-
|
|
1382
|
+
mime_type_str,
|
|
1259
1383
|
self._own_stream._stream
|
|
1260
1384
|
)
|
|
1261
1385
|
|
|
@@ -1273,13 +1397,13 @@ class Reader:
|
|
|
1273
1397
|
)
|
|
1274
1398
|
|
|
1275
1399
|
# Store the file to close it later
|
|
1276
|
-
self.
|
|
1400
|
+
self._backing_file = file
|
|
1277
1401
|
|
|
1278
1402
|
except Exception as e:
|
|
1279
1403
|
if self._own_stream:
|
|
1280
1404
|
self._own_stream.close()
|
|
1281
|
-
if hasattr(self, '
|
|
1282
|
-
self.
|
|
1405
|
+
if hasattr(self, '_backing_file') and self._backing_file:
|
|
1406
|
+
self._backing_file.close()
|
|
1283
1407
|
raise C2paError.Io(
|
|
1284
1408
|
Reader._ERROR_MESSAGES['io_error'].format(
|
|
1285
1409
|
str(e)))
|
|
@@ -1288,7 +1412,7 @@ class Reader:
|
|
|
1288
1412
|
# If stream is a string, treat it as a path and try to open it
|
|
1289
1413
|
|
|
1290
1414
|
# format_or_path is a format
|
|
1291
|
-
if format_or_path not in Reader.get_supported_mime_types():
|
|
1415
|
+
if format_or_path.lower() not in Reader.get_supported_mime_types():
|
|
1292
1416
|
raise C2paError.NotSupported(
|
|
1293
1417
|
f"Reader does not support {format_or_path}")
|
|
1294
1418
|
|
|
@@ -1297,11 +1421,11 @@ class Reader:
|
|
|
1297
1421
|
self._own_stream = Stream(file)
|
|
1298
1422
|
|
|
1299
1423
|
format_str = str(format_or_path)
|
|
1300
|
-
|
|
1424
|
+
format_bytes = format_str.encode('utf-8')
|
|
1301
1425
|
|
|
1302
1426
|
if manifest_data is None:
|
|
1303
1427
|
self._reader = _lib.c2pa_reader_from_stream(
|
|
1304
|
-
|
|
1428
|
+
format_bytes, self._own_stream._stream)
|
|
1305
1429
|
else:
|
|
1306
1430
|
if not isinstance(manifest_data, bytes):
|
|
1307
1431
|
raise TypeError(
|
|
@@ -1313,7 +1437,7 @@ class Reader:
|
|
|
1313
1437
|
manifest_data)
|
|
1314
1438
|
self._reader = (
|
|
1315
1439
|
_lib.c2pa_reader_from_manifest_data_and_stream(
|
|
1316
|
-
|
|
1440
|
+
format_bytes,
|
|
1317
1441
|
self._own_stream._stream,
|
|
1318
1442
|
manifest_array,
|
|
1319
1443
|
len(manifest_data),
|
|
@@ -1333,19 +1457,19 @@ class Reader:
|
|
|
1333
1457
|
)
|
|
1334
1458
|
)
|
|
1335
1459
|
|
|
1336
|
-
self.
|
|
1460
|
+
self._backing_file = file
|
|
1337
1461
|
except Exception as e:
|
|
1338
1462
|
if self._own_stream:
|
|
1339
1463
|
self._own_stream.close()
|
|
1340
|
-
if hasattr(self, '
|
|
1341
|
-
self.
|
|
1464
|
+
if hasattr(self, '_backing_file') and self._backing_file:
|
|
1465
|
+
self._backing_file.close()
|
|
1342
1466
|
raise C2paError.Io(
|
|
1343
1467
|
Reader._ERROR_MESSAGES['io_error'].format(
|
|
1344
1468
|
str(e)))
|
|
1345
1469
|
else:
|
|
1346
1470
|
# format_or_path is a format string
|
|
1347
1471
|
format_str = str(format_or_path)
|
|
1348
|
-
if format_str not in Reader.get_supported_mime_types():
|
|
1472
|
+
if format_str.lower() not in Reader.get_supported_mime_types():
|
|
1349
1473
|
raise C2paError.NotSupported(
|
|
1350
1474
|
f"Reader does not support {format_str}")
|
|
1351
1475
|
|
|
@@ -1386,11 +1510,85 @@ class Reader:
|
|
|
1386
1510
|
)
|
|
1387
1511
|
|
|
1388
1512
|
def __enter__(self):
|
|
1513
|
+
self._ensure_valid_state()
|
|
1514
|
+
|
|
1515
|
+
if not self._reader:
|
|
1516
|
+
raise C2paError("Invalid Reader when entering context")
|
|
1517
|
+
|
|
1389
1518
|
return self
|
|
1390
1519
|
|
|
1391
1520
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1392
1521
|
self.close()
|
|
1393
1522
|
|
|
1523
|
+
def __del__(self):
|
|
1524
|
+
"""Ensure resources are cleaned up if close() wasn't called.
|
|
1525
|
+
|
|
1526
|
+
This destructor handles cleanup without causing double frees.
|
|
1527
|
+
It only cleans up if the object hasn't been explicitly closed.
|
|
1528
|
+
"""
|
|
1529
|
+
self._cleanup_resources()
|
|
1530
|
+
|
|
1531
|
+
def _ensure_valid_state(self):
|
|
1532
|
+
"""Ensure the reader is in a valid state for operations.
|
|
1533
|
+
|
|
1534
|
+
Raises:
|
|
1535
|
+
C2paError: If the reader is closed or invalid
|
|
1536
|
+
"""
|
|
1537
|
+
if self._closed:
|
|
1538
|
+
raise C2paError(Reader._ERROR_MESSAGES['closed_error'])
|
|
1539
|
+
if not self._reader:
|
|
1540
|
+
raise C2paError(Reader._ERROR_MESSAGES['closed_error'])
|
|
1541
|
+
|
|
1542
|
+
def _cleanup_resources(self):
|
|
1543
|
+
"""Internal cleanup method that releases native resources.
|
|
1544
|
+
|
|
1545
|
+
This method handles the actual cleanup logic and can be called
|
|
1546
|
+
from both close() and __del__ without causing double frees.
|
|
1547
|
+
"""
|
|
1548
|
+
try:
|
|
1549
|
+
# Only cleanup if not already closed and we have a valid reader
|
|
1550
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
1551
|
+
self._closed = True
|
|
1552
|
+
|
|
1553
|
+
# Clean up reader
|
|
1554
|
+
if hasattr(self, '_reader') and self._reader:
|
|
1555
|
+
try:
|
|
1556
|
+
_lib.c2pa_reader_free(self._reader)
|
|
1557
|
+
except Exception:
|
|
1558
|
+
# Cleanup failure doesn't raise exceptions
|
|
1559
|
+
logger.error(
|
|
1560
|
+
"Failed to free native Reader resources"
|
|
1561
|
+
)
|
|
1562
|
+
pass
|
|
1563
|
+
finally:
|
|
1564
|
+
self._reader = None
|
|
1565
|
+
|
|
1566
|
+
# Clean up stream
|
|
1567
|
+
if hasattr(self, '_own_stream') and self._own_stream:
|
|
1568
|
+
try:
|
|
1569
|
+
self._own_stream.close()
|
|
1570
|
+
except Exception:
|
|
1571
|
+
# Cleanup failure doesn't raise exceptions
|
|
1572
|
+
logger.error("Failed to close Reader stream")
|
|
1573
|
+
pass
|
|
1574
|
+
finally:
|
|
1575
|
+
self._own_stream = None
|
|
1576
|
+
|
|
1577
|
+
# Clean up backing file (if needed)
|
|
1578
|
+
if self._backing_file:
|
|
1579
|
+
try:
|
|
1580
|
+
self._backing_file.close()
|
|
1581
|
+
except Exception:
|
|
1582
|
+
# Cleanup failure doesn't raise exceptions
|
|
1583
|
+
logger.warning("Failed to close Reader backing file")
|
|
1584
|
+
pass
|
|
1585
|
+
finally:
|
|
1586
|
+
self._backing_file = None
|
|
1587
|
+
|
|
1588
|
+
except Exception:
|
|
1589
|
+
# Ensure we don't raise exceptions during cleanup
|
|
1590
|
+
pass
|
|
1591
|
+
|
|
1394
1592
|
def close(self):
|
|
1395
1593
|
"""Release the reader resources.
|
|
1396
1594
|
|
|
@@ -1399,55 +1597,17 @@ class Reader:
|
|
|
1399
1597
|
Errors during cleanup are logged but not raised to ensure cleanup.
|
|
1400
1598
|
Multiple calls to close() are handled gracefully.
|
|
1401
1599
|
"""
|
|
1402
|
-
|
|
1403
|
-
# Track if we've already cleaned up
|
|
1404
|
-
if not hasattr(self, '_closed'):
|
|
1405
|
-
self._closed = False
|
|
1406
|
-
|
|
1407
1600
|
if self._closed:
|
|
1408
1601
|
return
|
|
1409
1602
|
|
|
1410
1603
|
try:
|
|
1411
|
-
#
|
|
1412
|
-
|
|
1413
|
-
try:
|
|
1414
|
-
_lib.c2pa_reader_free(self._reader)
|
|
1415
|
-
except Exception as e:
|
|
1416
|
-
print(
|
|
1417
|
-
Reader._ERROR_MESSAGES['reader_cleanup_error'].format(
|
|
1418
|
-
str(e)), file=sys.stderr)
|
|
1419
|
-
finally:
|
|
1420
|
-
self._reader = None
|
|
1421
|
-
|
|
1422
|
-
# Clean up stream
|
|
1423
|
-
if hasattr(self, '_own_stream') and self._own_stream:
|
|
1424
|
-
try:
|
|
1425
|
-
self._own_stream.close()
|
|
1426
|
-
except Exception as e:
|
|
1427
|
-
print(
|
|
1428
|
-
Reader._ERROR_MESSAGES['stream_error'].format(
|
|
1429
|
-
str(e)), file=sys.stderr)
|
|
1430
|
-
finally:
|
|
1431
|
-
self._own_stream = None
|
|
1432
|
-
|
|
1433
|
-
# Clean up file
|
|
1434
|
-
if hasattr(self, '_file_like_stream'):
|
|
1435
|
-
try:
|
|
1436
|
-
self._file_like_stream.close()
|
|
1437
|
-
except Exception as e:
|
|
1438
|
-
print(
|
|
1439
|
-
Reader._ERROR_MESSAGES['file_error'].format(
|
|
1440
|
-
str(e)), file=sys.stderr)
|
|
1441
|
-
finally:
|
|
1442
|
-
self._file_like_stream = None
|
|
1443
|
-
|
|
1444
|
-
# Clear any stored strings
|
|
1445
|
-
if hasattr(self, '_strings'):
|
|
1446
|
-
self._strings.clear()
|
|
1604
|
+
# Use the internal cleanup method
|
|
1605
|
+
self._cleanup_resources()
|
|
1447
1606
|
except Exception as e:
|
|
1448
|
-
|
|
1607
|
+
# Log any unexpected errors during close
|
|
1608
|
+
logger.error(
|
|
1449
1609
|
Reader._ERROR_MESSAGES['cleanup_error'].format(
|
|
1450
|
-
str(e))
|
|
1610
|
+
str(e)))
|
|
1451
1611
|
finally:
|
|
1452
1612
|
self._closed = True
|
|
1453
1613
|
|
|
@@ -1461,8 +1621,8 @@ class Reader:
|
|
|
1461
1621
|
C2paError: If there was an error getting the JSON
|
|
1462
1622
|
"""
|
|
1463
1623
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1624
|
+
self._ensure_valid_state()
|
|
1625
|
+
|
|
1466
1626
|
result = _lib.c2pa_reader_json(self._reader)
|
|
1467
1627
|
|
|
1468
1628
|
if result is None:
|
|
@@ -1486,13 +1646,12 @@ class Reader:
|
|
|
1486
1646
|
Raises:
|
|
1487
1647
|
C2paError: If there was an error writing the resource to stream
|
|
1488
1648
|
"""
|
|
1489
|
-
|
|
1490
|
-
raise C2paError("Reader is closed")
|
|
1649
|
+
self._ensure_valid_state()
|
|
1491
1650
|
|
|
1492
|
-
|
|
1651
|
+
uri_str = uri.encode('utf-8')
|
|
1493
1652
|
with Stream(stream) as stream_obj:
|
|
1494
1653
|
result = _lib.c2pa_reader_resource_to_stream(
|
|
1495
|
-
self._reader,
|
|
1654
|
+
self._reader, uri_str, stream_obj._stream)
|
|
1496
1655
|
|
|
1497
1656
|
if result < 0:
|
|
1498
1657
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
@@ -1526,7 +1685,17 @@ class Signer:
|
|
|
1526
1685
|
|
|
1527
1686
|
Note: This constructor is not meant to be called directly.
|
|
1528
1687
|
Use from_info() or from_callback() instead.
|
|
1688
|
+
|
|
1689
|
+
Args:
|
|
1690
|
+
signer_ptr: Pointer to the native C2PA signer
|
|
1691
|
+
|
|
1692
|
+
Raises:
|
|
1693
|
+
C2paError: If the signer pointer is invalid
|
|
1529
1694
|
"""
|
|
1695
|
+
# Validate pointer before assignment
|
|
1696
|
+
if not signer_ptr:
|
|
1697
|
+
raise C2paError("Invalid signer pointer: pointer is null")
|
|
1698
|
+
|
|
1530
1699
|
self._signer = signer_ptr
|
|
1531
1700
|
self._closed = False
|
|
1532
1701
|
|
|
@@ -1652,9 +1821,9 @@ class Signer:
|
|
|
1652
1821
|
# Native code expects the signed len to be returned, we oblige
|
|
1653
1822
|
return actual_len
|
|
1654
1823
|
except Exception as e:
|
|
1655
|
-
|
|
1824
|
+
logger.error(
|
|
1656
1825
|
cls._ERROR_MESSAGES['callback_error'].format(
|
|
1657
|
-
str(e))
|
|
1826
|
+
str(e)))
|
|
1658
1827
|
# Error: exception raised, invalid so return -1,
|
|
1659
1828
|
# native code will handle the error when seeing -1
|
|
1660
1829
|
return -1
|
|
@@ -1701,40 +1870,85 @@ class Signer:
|
|
|
1701
1870
|
|
|
1702
1871
|
def __enter__(self):
|
|
1703
1872
|
"""Context manager entry."""
|
|
1704
|
-
|
|
1705
|
-
|
|
1873
|
+
self._ensure_valid_state()
|
|
1874
|
+
|
|
1875
|
+
if not self._signer:
|
|
1876
|
+
raise C2paError("Invalid signer pointer: pointer is null")
|
|
1877
|
+
|
|
1706
1878
|
return self
|
|
1707
1879
|
|
|
1708
1880
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1709
1881
|
"""Context manager exit."""
|
|
1710
1882
|
self.close()
|
|
1711
1883
|
|
|
1884
|
+
def _cleanup_resources(self):
|
|
1885
|
+
"""Internal cleanup method that releases native resources.
|
|
1886
|
+
|
|
1887
|
+
This method handles the actual cleanup logic and can be called
|
|
1888
|
+
from both close() and __del__ without causing double frees.
|
|
1889
|
+
"""
|
|
1890
|
+
try:
|
|
1891
|
+
if not self._closed and self._signer:
|
|
1892
|
+
self._closed = True
|
|
1893
|
+
|
|
1894
|
+
try:
|
|
1895
|
+
_lib.c2pa_signer_free(self._signer)
|
|
1896
|
+
except Exception:
|
|
1897
|
+
# Log cleanup errors but don't raise exceptions
|
|
1898
|
+
logger.error("Failed to free native Signer resources")
|
|
1899
|
+
finally:
|
|
1900
|
+
self._signer = None
|
|
1901
|
+
|
|
1902
|
+
# Clean up callback reference
|
|
1903
|
+
if self._callback_cb:
|
|
1904
|
+
self._callback_cb = None
|
|
1905
|
+
|
|
1906
|
+
except Exception:
|
|
1907
|
+
# Ensure we don't raise exceptions during cleanup
|
|
1908
|
+
pass
|
|
1909
|
+
|
|
1910
|
+
def _ensure_valid_state(self):
|
|
1911
|
+
"""Ensure the signer is in a valid state for operations.
|
|
1912
|
+
|
|
1913
|
+
Raises:
|
|
1914
|
+
C2paError: If the signer is closed or invalid
|
|
1915
|
+
"""
|
|
1916
|
+
if self._closed:
|
|
1917
|
+
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1918
|
+
if not self._signer:
|
|
1919
|
+
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1920
|
+
|
|
1712
1921
|
def close(self):
|
|
1713
1922
|
"""Release the signer resources.
|
|
1714
1923
|
|
|
1715
1924
|
This method ensures all resources are properly cleaned up,
|
|
1716
1925
|
even if errors occur during cleanup.
|
|
1717
|
-
|
|
1718
|
-
|
|
1926
|
+
|
|
1927
|
+
Note:
|
|
1928
|
+
Multiple calls to close() are handled gracefully.
|
|
1929
|
+
Errors during cleanup are logged but not raised
|
|
1930
|
+
to ensure cleanup.
|
|
1719
1931
|
"""
|
|
1720
1932
|
if self._closed:
|
|
1721
1933
|
return
|
|
1722
1934
|
|
|
1723
1935
|
try:
|
|
1724
|
-
if
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1936
|
+
# Validate pointer before cleanup if it exists
|
|
1937
|
+
if self._signer and self._signer != 0:
|
|
1938
|
+
# Use the internal cleanup method
|
|
1939
|
+
self._cleanup_resources()
|
|
1940
|
+
else:
|
|
1941
|
+
# Make sure to release the callback
|
|
1942
|
+
if self._callback_cb:
|
|
1943
|
+
self._callback_cb = None
|
|
1944
|
+
|
|
1733
1945
|
except Exception as e:
|
|
1734
|
-
|
|
1946
|
+
# Log any unexpected errors during close
|
|
1947
|
+
logger.error(
|
|
1735
1948
|
Signer._ERROR_MESSAGES['cleanup_error'].format(
|
|
1736
|
-
str(e))
|
|
1949
|
+
str(e)))
|
|
1737
1950
|
finally:
|
|
1951
|
+
# Always mark as closed, regardless of cleanup success
|
|
1738
1952
|
self._closed = True
|
|
1739
1953
|
|
|
1740
1954
|
def reserve_size(self) -> int:
|
|
@@ -1746,8 +1960,7 @@ class Signer:
|
|
|
1746
1960
|
Raises:
|
|
1747
1961
|
C2paError: If there was an error getting the size
|
|
1748
1962
|
"""
|
|
1749
|
-
|
|
1750
|
-
raise C2paError(Signer._ERROR_MESSAGES['closed_error'])
|
|
1963
|
+
self._ensure_valid_state()
|
|
1751
1964
|
|
|
1752
1965
|
result = _lib.c2pa_signer_reserve_size(self._signer)
|
|
1753
1966
|
|
|
@@ -1793,22 +2006,65 @@ class Builder:
|
|
|
1793
2006
|
|
|
1794
2007
|
@classmethod
|
|
1795
2008
|
def get_supported_mime_types(cls) -> list[str]:
|
|
2009
|
+
"""Get the list of supported MIME types for the Builder.
|
|
2010
|
+
This method retrieves supported MIME types from the native library.
|
|
2011
|
+
|
|
2012
|
+
Returns:
|
|
2013
|
+
List of supported MIME type strings
|
|
2014
|
+
|
|
2015
|
+
Raises:
|
|
2016
|
+
C2paError: If there was an error retrieving the MIME types
|
|
2017
|
+
"""
|
|
1796
2018
|
if cls._supported_mime_types_cache is not None:
|
|
1797
2019
|
return cls._supported_mime_types_cache
|
|
1798
2020
|
|
|
1799
2021
|
count = ctypes.c_size_t()
|
|
1800
2022
|
arr = _lib.c2pa_builder_supported_mime_types(ctypes.byref(count))
|
|
1801
2023
|
|
|
2024
|
+
# Validate the returned array pointer
|
|
2025
|
+
if not arr:
|
|
2026
|
+
# If no array returned, check for errors
|
|
2027
|
+
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
2028
|
+
if error:
|
|
2029
|
+
raise C2paError(f"Failed to get supported MIME types: {error}")
|
|
2030
|
+
# Return empty list if no error but no array
|
|
2031
|
+
return []
|
|
2032
|
+
|
|
2033
|
+
# Validate count value
|
|
2034
|
+
if count.value <= 0:
|
|
2035
|
+
# Free the array even if count is invalid
|
|
2036
|
+
try:
|
|
2037
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
2038
|
+
except Exception:
|
|
2039
|
+
pass
|
|
2040
|
+
return []
|
|
2041
|
+
|
|
1802
2042
|
try:
|
|
1803
|
-
|
|
1804
|
-
|
|
2043
|
+
result = []
|
|
2044
|
+
for i in range(count.value):
|
|
2045
|
+
try:
|
|
2046
|
+
# Validate each array element before accessing
|
|
2047
|
+
if arr[i] is None:
|
|
2048
|
+
continue
|
|
2049
|
+
|
|
2050
|
+
mime_type = arr[i].decode("utf-8", errors='replace')
|
|
2051
|
+
if mime_type:
|
|
2052
|
+
result.append(mime_type)
|
|
2053
|
+
except Exception:
|
|
2054
|
+
# Ignore decoding failures
|
|
2055
|
+
continue
|
|
1805
2056
|
finally:
|
|
1806
|
-
#
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2057
|
+
# Always free the native memory, even if string extraction fails
|
|
2058
|
+
try:
|
|
2059
|
+
_lib.c2pa_free_string_array(arr, count.value)
|
|
2060
|
+
except Exception:
|
|
2061
|
+
# Ignore cleanup errors
|
|
2062
|
+
pass
|
|
2063
|
+
|
|
2064
|
+
# Cache the result
|
|
2065
|
+
if result:
|
|
2066
|
+
cls._supported_mime_types_cache = result
|
|
1810
2067
|
|
|
1811
|
-
cls._supported_mime_types_cache = result
|
|
1812
2068
|
return cls._supported_mime_types_cache
|
|
1813
2069
|
|
|
1814
2070
|
def __init__(self, manifest_json: Any):
|
|
@@ -1822,6 +2078,7 @@ class Builder:
|
|
|
1822
2078
|
C2paError.Encoding: If manifest JSON contains invalid UTF-8 chars
|
|
1823
2079
|
C2paError.Json: If the manifest JSON cannot be serialized
|
|
1824
2080
|
"""
|
|
2081
|
+
self._closed = False
|
|
1825
2082
|
self._builder = None
|
|
1826
2083
|
|
|
1827
2084
|
if not isinstance(manifest_json, str):
|
|
@@ -1885,6 +2142,9 @@ class Builder:
|
|
|
1885
2142
|
builder._builder = _lib.c2pa_builder_from_archive(stream_obj._stream)
|
|
1886
2143
|
|
|
1887
2144
|
if not builder._builder:
|
|
2145
|
+
# Clean up the stream object if builder creation fails
|
|
2146
|
+
stream_obj.close()
|
|
2147
|
+
|
|
1888
2148
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
1889
2149
|
if error:
|
|
1890
2150
|
raise C2paError(error)
|
|
@@ -1892,10 +2152,48 @@ class Builder:
|
|
|
1892
2152
|
|
|
1893
2153
|
return builder
|
|
1894
2154
|
|
|
2155
|
+
def _ensure_valid_state(self):
|
|
2156
|
+
"""Ensure the builder is in a valid state for operations.
|
|
2157
|
+
|
|
2158
|
+
Raises:
|
|
2159
|
+
C2paError: If the builder is closed or invalid
|
|
2160
|
+
"""
|
|
2161
|
+
if self._closed:
|
|
2162
|
+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2163
|
+
if not self._builder:
|
|
2164
|
+
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2165
|
+
|
|
2166
|
+
def _cleanup_resources(self):
|
|
2167
|
+
"""Internal cleanup method that releases native resources.
|
|
2168
|
+
|
|
2169
|
+
This method handles the actual cleanup logic and can be called
|
|
2170
|
+
from both close() and __del__ without causing double frees.
|
|
2171
|
+
"""
|
|
2172
|
+
try:
|
|
2173
|
+
# Only cleanup if not already closed and we have a valid builder
|
|
2174
|
+
if hasattr(self, '_closed') and not self._closed:
|
|
2175
|
+
self._closed = True
|
|
2176
|
+
|
|
2177
|
+
if hasattr(
|
|
2178
|
+
self,
|
|
2179
|
+
'_builder') and self._builder and self._builder != 0:
|
|
2180
|
+
try:
|
|
2181
|
+
_lib.c2pa_builder_free(self._builder)
|
|
2182
|
+
except Exception:
|
|
2183
|
+
# Log cleanup errors but don't raise exceptions
|
|
2184
|
+
logger.error(
|
|
2185
|
+
"Failed to release native Builder resources"
|
|
2186
|
+
)
|
|
2187
|
+
pass
|
|
2188
|
+
finally:
|
|
2189
|
+
self._builder = None
|
|
2190
|
+
except Exception:
|
|
2191
|
+
# Ensure we don't raise exceptions during cleanup
|
|
2192
|
+
pass
|
|
2193
|
+
|
|
1895
2194
|
def __del__(self):
|
|
1896
2195
|
"""Ensure resources are cleaned up if close() wasn't called."""
|
|
1897
|
-
|
|
1898
|
-
self.close()
|
|
2196
|
+
self._cleanup_resources()
|
|
1899
2197
|
|
|
1900
2198
|
def close(self):
|
|
1901
2199
|
"""Release the builder resources.
|
|
@@ -1905,32 +2203,26 @@ class Builder:
|
|
|
1905
2203
|
Errors during cleanup are logged but not raised to ensure cleanup.
|
|
1906
2204
|
Multiple calls to close() are handled gracefully.
|
|
1907
2205
|
"""
|
|
1908
|
-
# Track if we've already cleaned up
|
|
1909
|
-
if not hasattr(self, '_closed'):
|
|
1910
|
-
self._closed = False
|
|
1911
|
-
|
|
1912
2206
|
if self._closed:
|
|
1913
2207
|
return
|
|
1914
2208
|
|
|
1915
2209
|
try:
|
|
1916
|
-
#
|
|
1917
|
-
|
|
1918
|
-
try:
|
|
1919
|
-
_lib.c2pa_builder_free(self._builder)
|
|
1920
|
-
except Exception as e:
|
|
1921
|
-
print(
|
|
1922
|
-
Builder._ERROR_MESSAGES['builder_cleanup'].format(
|
|
1923
|
-
str(e)), file=sys.stderr)
|
|
1924
|
-
finally:
|
|
1925
|
-
self._builder = None
|
|
2210
|
+
# Use the internal cleanup method
|
|
2211
|
+
self._cleanup_resources()
|
|
1926
2212
|
except Exception as e:
|
|
1927
|
-
|
|
2213
|
+
# Log any unexpected errors during close
|
|
2214
|
+
logger.error(
|
|
1928
2215
|
Builder._ERROR_MESSAGES['cleanup_error'].format(
|
|
1929
|
-
str(e))
|
|
2216
|
+
str(e)))
|
|
1930
2217
|
finally:
|
|
1931
2218
|
self._closed = True
|
|
1932
2219
|
|
|
1933
2220
|
def __enter__(self):
|
|
2221
|
+
self._ensure_valid_state()
|
|
2222
|
+
|
|
2223
|
+
if not self._builder:
|
|
2224
|
+
raise C2paError("Invalid Builder when entering context")
|
|
2225
|
+
|
|
1934
2226
|
return self
|
|
1935
2227
|
|
|
1936
2228
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
@@ -1943,9 +2235,7 @@ class Builder:
|
|
|
1943
2235
|
into the asset when signing.
|
|
1944
2236
|
This is useful when creating cloud or sidecar manifests.
|
|
1945
2237
|
"""
|
|
1946
|
-
|
|
1947
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
1948
|
-
|
|
2238
|
+
self._ensure_valid_state()
|
|
1949
2239
|
_lib.c2pa_builder_set_no_embed(self._builder)
|
|
1950
2240
|
|
|
1951
2241
|
def set_remote_url(self, remote_url: str):
|
|
@@ -1960,8 +2250,7 @@ class Builder:
|
|
|
1960
2250
|
Raises:
|
|
1961
2251
|
C2paError: If there was an error setting the remote URL
|
|
1962
2252
|
"""
|
|
1963
|
-
|
|
1964
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2253
|
+
self._ensure_valid_state()
|
|
1965
2254
|
|
|
1966
2255
|
url_str = remote_url.encode('utf-8')
|
|
1967
2256
|
result = _lib.c2pa_builder_set_remote_url(self._builder, url_str)
|
|
@@ -1984,8 +2273,7 @@ class Builder:
|
|
|
1984
2273
|
Raises:
|
|
1985
2274
|
C2paError: If there was an error adding the resource
|
|
1986
2275
|
"""
|
|
1987
|
-
|
|
1988
|
-
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2276
|
+
self._ensure_valid_state()
|
|
1989
2277
|
|
|
1990
2278
|
uri_str = uri.encode('utf-8')
|
|
1991
2279
|
with Stream(stream) as stream_obj:
|
|
@@ -2167,7 +2455,11 @@ class Builder:
|
|
|
2167
2455
|
if not self._builder:
|
|
2168
2456
|
raise C2paError(Builder._ERROR_MESSAGES['closed_error'])
|
|
2169
2457
|
|
|
2170
|
-
|
|
2458
|
+
# Validate signer pointer before use
|
|
2459
|
+
if not signer or not hasattr(signer, '_signer') or not signer._signer:
|
|
2460
|
+
raise C2paError("Invalid or closed signer")
|
|
2461
|
+
|
|
2462
|
+
if format.lower() not in Builder.get_supported_mime_types():
|
|
2171
2463
|
raise C2paError.NotSupported(
|
|
2172
2464
|
f"Builder does not support {format}")
|
|
2173
2465
|
|
|
@@ -2175,14 +2467,18 @@ class Builder:
|
|
|
2175
2467
|
manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)()
|
|
2176
2468
|
|
|
2177
2469
|
# c2pa_builder_sign uses streams
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2470
|
+
try:
|
|
2471
|
+
result = _lib.c2pa_builder_sign(
|
|
2472
|
+
self._builder,
|
|
2473
|
+
format_str,
|
|
2474
|
+
source_stream._stream,
|
|
2475
|
+
dest_stream._stream,
|
|
2476
|
+
signer._signer,
|
|
2477
|
+
ctypes.byref(manifest_bytes_ptr)
|
|
2478
|
+
)
|
|
2479
|
+
except Exception as e:
|
|
2480
|
+
# Handle errors during the C function call
|
|
2481
|
+
raise C2paError(f"Error calling c2pa_builder_sign: {str(e)}")
|
|
2186
2482
|
|
|
2187
2483
|
if result < 0:
|
|
2188
2484
|
error = _parse_operation_result_for_error(_lib.c2pa_error())
|
|
@@ -2199,8 +2495,6 @@ class Builder:
|
|
|
2199
2495
|
ctypes.memmove(temp_buffer, manifest_bytes_ptr, result)
|
|
2200
2496
|
manifest_bytes = bytes(temp_buffer)
|
|
2201
2497
|
except Exception:
|
|
2202
|
-
# If there's any error accessing the memory, just return
|
|
2203
|
-
# empty bytes
|
|
2204
2498
|
manifest_bytes = b""
|
|
2205
2499
|
finally:
|
|
2206
2500
|
# Always free the C-allocated memory,
|
|
@@ -2208,7 +2502,9 @@ class Builder:
|
|
|
2208
2502
|
try:
|
|
2209
2503
|
_lib.c2pa_manifest_bytes_free(manifest_bytes_ptr)
|
|
2210
2504
|
except Exception:
|
|
2211
|
-
|
|
2505
|
+
logger.error(
|
|
2506
|
+
"Failed to release native manifest bytes memory"
|
|
2507
|
+
)
|
|
2212
2508
|
pass
|
|
2213
2509
|
|
|
2214
2510
|
return manifest_bytes
|
|
@@ -2269,7 +2565,9 @@ class Builder:
|
|
|
2269
2565
|
"""Sign a file and write the signed data to an output file.
|
|
2270
2566
|
|
|
2271
2567
|
Args:
|
|
2272
|
-
source_path: Path to the source file
|
|
2568
|
+
source_path: Path to the source file. We will attempt
|
|
2569
|
+
to guess the mimetype of the source file based on
|
|
2570
|
+
the extension.
|
|
2273
2571
|
dest_path: Path to write the signed file to
|
|
2274
2572
|
signer: The signer to use
|
|
2275
2573
|
|
|
@@ -2280,10 +2578,7 @@ class Builder:
|
|
|
2280
2578
|
C2paError: If there was an error during signing
|
|
2281
2579
|
"""
|
|
2282
2580
|
# Get the MIME type from the file extension
|
|
2283
|
-
mime_type =
|
|
2284
|
-
if not mime_type:
|
|
2285
|
-
raise C2paError.NotSupported(
|
|
2286
|
-
f"Could not determine MIME type for file: {source_path}")
|
|
2581
|
+
mime_type = _get_mime_type_from_path(source_path)
|
|
2287
2582
|
|
|
2288
2583
|
try:
|
|
2289
2584
|
# Open source file and destination file, then use the sign method
|
|
@@ -16,8 +16,9 @@ from enum import Enum
|
|
|
16
16
|
# Debug flag for library loading
|
|
17
17
|
DEBUG_LIBRARY_LOADING = False
|
|
18
18
|
|
|
19
|
-
# Create a module-specific logger with NullHandler
|
|
20
|
-
|
|
19
|
+
# Create a module-specific logger with NullHandler
|
|
20
|
+
# to avoid interfering with global configuration
|
|
21
|
+
logger = logging.getLogger("c2pa.loader")
|
|
21
22
|
logger.addHandler(logging.NullHandler())
|
|
22
23
|
|
|
23
24
|
|
|
@@ -107,8 +108,7 @@ def _load_single_library(lib_name: str,
|
|
|
107
108
|
The loaded library or None if loading failed
|
|
108
109
|
"""
|
|
109
110
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
110
|
-
logger.info(
|
|
111
|
-
f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}")
|
|
111
|
+
logger.info(f"Searching for library '{lib_name}' in paths: {[str(p) for p in search_paths]}")
|
|
112
112
|
current_arch = _get_architecture()
|
|
113
113
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
114
114
|
logger.info(f"Current architecture: {current_arch}")
|
|
@@ -125,12 +125,10 @@ def _load_single_library(lib_name: str,
|
|
|
125
125
|
except Exception as e:
|
|
126
126
|
error_msg = str(e)
|
|
127
127
|
if "incompatible architecture" in error_msg:
|
|
128
|
-
logger.error(
|
|
129
|
-
f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}")
|
|
128
|
+
logger.error(f"Architecture mismatch: Library at {lib_path} is not compatible with current architecture {current_arch}")
|
|
130
129
|
logger.error(f"Error details: {error_msg}")
|
|
131
130
|
else:
|
|
132
|
-
logger.error(
|
|
133
|
-
f"Failed to load library from {lib_path}: {e}")
|
|
131
|
+
logger.error(f"Failed to load library from {lib_path}: {e}")
|
|
134
132
|
else:
|
|
135
133
|
logger.debug(f"Library not found at: {lib_path}")
|
|
136
134
|
return None
|
|
@@ -216,16 +214,14 @@ def dynamically_load_library(
|
|
|
216
214
|
env_lib_name = os.environ.get("C2PA_LIBRARY_NAME")
|
|
217
215
|
if env_lib_name:
|
|
218
216
|
if DEBUG_LIBRARY_LOADING: # pragma: no cover
|
|
219
|
-
logger.info(
|
|
220
|
-
f"Using library name from env var C2PA_LIBRARY_NAME: {env_lib_name}")
|
|
217
|
+
logger.info(f"Using library name from env var C2PA_LIBRARY_NAME: {env_lib_name}")
|
|
221
218
|
try:
|
|
222
219
|
possible_paths = _get_possible_search_paths()
|
|
223
220
|
lib = _load_single_library(env_lib_name, possible_paths)
|
|
224
221
|
if lib:
|
|
225
222
|
return lib
|
|
226
223
|
else:
|
|
227
|
-
logger.error(
|
|
228
|
-
f"Could not find library {env_lib_name} in any of the search paths")
|
|
224
|
+
logger.error(f"Could not find library {env_lib_name} in any of the search paths")
|
|
229
225
|
# Continue with normal loading if environment variable library
|
|
230
226
|
# name fails
|
|
231
227
|
except Exception as e:
|
|
@@ -241,12 +237,9 @@ def dynamically_load_library(
|
|
|
241
237
|
if not lib:
|
|
242
238
|
platform_id = get_platform_identifier()
|
|
243
239
|
current_arch = _get_architecture()
|
|
244
|
-
logger.error(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
248
|
-
raise RuntimeError(
|
|
249
|
-
f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})")
|
|
240
|
+
logger.error(f"Could not find {lib_name} in any of the search paths: {[str(p) for p in possible_paths]}")
|
|
241
|
+
logger.error(f"Platform: {platform_id}, Architecture: {current_arch}")
|
|
242
|
+
raise RuntimeError(f"Could not find {lib_name} in any of the search paths (Platform: {platform_id}, Architecture: {current_arch})")
|
|
250
243
|
return lib
|
|
251
244
|
|
|
252
245
|
# Default path (no library name provided in the environment)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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>
|
|
@@ -126,9 +126,6 @@ class TestReader(unittest.TestCase):
|
|
|
126
126
|
def test_reader_close_cleanup(self):
|
|
127
127
|
with open(self.testPath, "rb") as file:
|
|
128
128
|
reader = Reader("image/jpeg", file)
|
|
129
|
-
# Store references to internal objects
|
|
130
|
-
reader_ref = reader._reader
|
|
131
|
-
stream_ref = reader._own_stream
|
|
132
129
|
# Close the reader
|
|
133
130
|
reader.close()
|
|
134
131
|
# Verify all resources are cleaned up
|
|
@@ -144,6 +141,32 @@ class TestReader(unittest.TestCase):
|
|
|
144
141
|
with self.assertRaises(Error):
|
|
145
142
|
reader.resource_to_stream("", io.BytesIO(bytearray()))
|
|
146
143
|
|
|
144
|
+
def test_read_dng_from_stream(self):
|
|
145
|
+
test_path = os.path.join(self.data_dir, "C.dng")
|
|
146
|
+
with open(test_path, "rb") as file:
|
|
147
|
+
file_content = file.read()
|
|
148
|
+
|
|
149
|
+
with Reader("dng", io.BytesIO(file_content)) as reader:
|
|
150
|
+
# Just run and verify there is no crash
|
|
151
|
+
json.loads(reader.json())
|
|
152
|
+
|
|
153
|
+
def test_read_dng_upper_case_from_stream(self):
|
|
154
|
+
test_path = os.path.join(self.data_dir, "C.dng")
|
|
155
|
+
with open(test_path, "rb") as file:
|
|
156
|
+
file_content = file.read()
|
|
157
|
+
|
|
158
|
+
with Reader("DNG", io.BytesIO(file_content)) as reader:
|
|
159
|
+
# Just run and verify there is no crash
|
|
160
|
+
json.loads(reader.json())
|
|
161
|
+
|
|
162
|
+
def test_read_dng_file_from_path(self):
|
|
163
|
+
test_path = os.path.join(self.data_dir, "C.dng")
|
|
164
|
+
|
|
165
|
+
# Create reader with the file content
|
|
166
|
+
with Reader(test_path) as reader:
|
|
167
|
+
# Just run and verify there is no crash
|
|
168
|
+
json.loads(reader.json())
|
|
169
|
+
|
|
147
170
|
def test_read_all_files(self):
|
|
148
171
|
"""Test reading C2PA metadata from all files in the fixtures/files-for-reading-tests directory"""
|
|
149
172
|
reading_dir = os.path.join(self.data_dir, "files-for-reading-tests")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|