c2pa-python 0.37.1__tar.gz → 0.37.2__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.37.1/src/c2pa_python.egg-info → c2pa_python-0.37.2}/PKG-INFO +1 -1
  2. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/pyproject.toml +1 -1
  3. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa/c2pa.py +196 -87
  4. {c2pa_python-0.37.1 → c2pa_python-0.37.2/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  5. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/tests/test_unit_tests.py +754 -23
  6. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/LICENSE-APACHE +0 -0
  7. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/LICENSE-MIT +0 -0
  8. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/MANIFEST.in +0 -0
  9. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/README.md +0 -0
  10. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/requirements.txt +0 -0
  11. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/scripts/download_artifacts.py +0 -0
  12. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/setup.cfg +0 -0
  13. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/setup.py +0 -0
  14. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa/__init__.py +0 -0
  15. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa/build.py +0 -0
  16. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.37.1 → c2pa_python-0.37.2}/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.37.1
3
+ Version: 0.37.2
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>, Tania Mathern <mathern@adobe.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "c2pa-python"
7
- version = "0.37.1"
7
+ version = "0.37.2"
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" }
@@ -11,7 +11,7 @@
11
11
  # specific language governing permissions and limitations under
12
12
  # each license.
13
13
 
14
- # Version: 0.37.1
14
+ # Version: 0.37.2
15
15
 
16
16
  import ctypes
17
17
  import enum
@@ -1196,16 +1196,21 @@ def load_settings(settings: Union[str, dict], format: str = "json") -> None:
1196
1196
 
1197
1197
 
1198
1198
  def _get_mime_type_from_path(path: Union[str, Path]) -> str:
1199
- """Attempt to guess the MIME type from a file path (with extension).
1199
+ """Attempt to guess the MIME type from a file path's extension.
1200
+ When the extension is missing or unrecognized, this returns an empty
1201
+ string so the caller hands it to the lib for auto-detection.
1202
+ A recognized-but-wrong extension still returns a mimetype:
1203
+ the native layer will attempt to correct it from the bytes when
1204
+ reading (the real type still needs to be supported by the lib),
1205
+ and if it can't, an error will happen then.
1200
1206
 
1201
1207
  Args:
1202
1208
  path: File path as string or Path object
1203
1209
 
1204
1210
  Returns:
1205
- MIME type string
1206
-
1207
- Raises:
1208
- C2paError.NotSupported: If MIME type cannot be determined
1211
+ MIME type string, or an empty string
1212
+ (when it cannot be determined from the extension).
1213
+ An empty string here means the native lib should attempt auto-detect.
1209
1214
  """
1210
1215
  path_obj = Path(path)
1211
1216
  file_extension = path_obj.suffix.lower() if path_obj.suffix else ""
@@ -1215,11 +1220,9 @@ def _get_mime_type_from_path(path: Union[str, Path]) -> str:
1215
1220
  # so we bypass it and set the correct type
1216
1221
  return "image/dng"
1217
1222
  else:
1218
- mime_type = mimetypes.guess_type(str(path))[0]
1219
- if not mime_type:
1220
- raise C2paError.NotSupported(
1221
- f"Could not determine MIME type for file: {path}")
1222
- return mime_type
1223
+ # Fall back to an empty string for extensionless or unknown files.
1224
+ # Empty string flags this as a guess-type attempt.
1225
+ return mimetypes.guess_type(str(path))[0] or ""
1223
1226
 
1224
1227
 
1225
1228
  class ContextProvider(ABC):
@@ -1567,6 +1570,9 @@ class Stream:
1567
1570
  # Maximum value for a 32-bit signed integer (2^31 - 1)
1568
1571
  _MAX_STREAM_ID = 2**31 - 1
1569
1572
 
1573
+ # Methods an object must expose to be wrapped as Stream.
1574
+ _REQUIRED_STREAM_METHODS = ("read", "write", "seek", "tell", "flush")
1575
+
1570
1576
  # Class-level error messages to avoid multiple creation
1571
1577
  _ERROR_MESSAGES = {
1572
1578
  'stream_error': "Error cleaning up stream: {}",
@@ -1613,15 +1619,14 @@ class Stream:
1613
1619
  self._stream_id = f"{id(self)}-{stream_counter}"
1614
1620
 
1615
1621
  # Rest of the existing initialization code...
1616
- required_methods = ['read', 'write', 'seek', 'tell', 'flush']
1617
1622
  missing_methods = [
1618
- method for method in required_methods if not hasattr(
1623
+ method for method in Stream._REQUIRED_STREAM_METHODS if not hasattr(
1619
1624
  file_like_stream, method)]
1620
1625
  if missing_methods:
1621
1626
  raise TypeError(
1622
1627
  "Object must be a stream-like object with methods: {}. "
1623
1628
  "Missing: {}".format(
1624
- ", ".join(required_methods),
1629
+ ", ".join(Stream._REQUIRED_STREAM_METHODS),
1625
1630
  ", ".join(missing_methods),
1626
1631
  )
1627
1632
  )
@@ -1949,7 +1954,8 @@ def _get_supported_mime_types(ffi_func, cache):
1949
1954
  try:
1950
1955
  if arr[i] is None:
1951
1956
  continue
1952
- mime_type = arr[i].decode("utf-8", errors='replace')
1957
+ mime_type = arr[i].decode(
1958
+ "utf-8", errors='replace').strip().lower()
1953
1959
  if mime_type:
1954
1960
  result.append(mime_type)
1955
1961
  except Exception:
@@ -1968,33 +1974,75 @@ def _get_supported_mime_types(ffi_func, cache):
1968
1974
  return [], cache
1969
1975
 
1970
1976
 
1971
- def _validate_and_encode_format(
1972
- format_str: str, supported_types: list[str], class_name: str
1973
- ) -> bytes:
1974
- """Validate a MIME type / format string and encode it to UTF-8 bytes.
1977
+ def _encode_format(
1978
+ format_str: Optional[str],
1979
+ class_name: str,
1980
+ allow_autodetect: bool = True,
1981
+ ) -> Optional[bytes]:
1982
+ """Normalize a MIME type/format string and encode it to UTF-8 bytes.
1983
+
1984
+ The binding does not validate the format itself: the native library is the
1985
+ authority on what is supported and detects the asset type from the bytes,
1986
+ so the format is passed straight through after trimming whitespace.
1987
+
1988
+ None, an empty string, or whitespace all mean the same thing: no format was
1989
+ given. When allow_autodetect is True (default), that requests detection and
1990
+ the native library guesses the type from the bytes. When allow_autodetect
1991
+ is False, a missing format is rejected instead.
1975
1992
 
1976
1993
  Args:
1977
- format_str: The MIME type or format string to validate
1978
- supported_types: List of supported MIME types
1979
- class_name: Name of the calling class (for error messages)
1994
+ format_str: The MIME type or format, or None. Pass None, an empty
1995
+ string, or whitespace to request detection from the asset's bytes
1996
+ (only when ``allow_autodetect`` is True).
1997
+ class_name: Name of the calling class (for error messages).
1998
+ allow_autodetect: When True (default), a missing format requests
1999
+ detection. When False, a missing format raises
2000
+ C2paError.NotSupported.
1980
2001
 
1981
2002
  Returns:
1982
- UTF-8 encoded format bytes
2003
+ The lowercased UTF-8 format bytes, or None to request auto-detection
2004
+ (no format given). Use _format_ffi_arg to turn the result into the
2005
+ empty-bytes flag the native library expects.
1983
2006
 
1984
2007
  Raises:
1985
- C2paError.NotSupported: If the format is not supported
1986
- C2paError.Encoding: If the string contains invalid UTF-8 characters
2008
+ C2paError.NotSupported: If the format is missing and
2009
+ ``allow_autodetect`` is False.
2010
+ C2paError.Encoding: If the format contains invalid UTF-8 characters.
1987
2011
  """
1988
- if format_str.lower() not in supported_types:
2012
+ key = (format_str or "").strip().lower()
2013
+ if not key:
2014
+ if allow_autodetect:
2015
+ return None
1989
2016
  raise C2paError.NotSupported(
1990
- f"{class_name} does not support {format_str}")
2017
+ f"{class_name} requires an explicit format (MIME type)"
2018
+ )
1991
2019
  try:
1992
- return format_str.encode('utf-8')
2020
+ return key.encode('utf-8')
1993
2021
  except UnicodeError as e:
1994
2022
  raise C2paError.Encoding(
1995
2023
  f"Invalid UTF-8 characters in input: {e}")
1996
2024
 
1997
2025
 
2026
+ def _format_ffi_arg(fmt: Optional[bytes]) -> bytes:
2027
+ """Convert an encoded format to the native FFI argument.
2028
+
2029
+ The native library treats empty bytes as the "detect the type from the
2030
+ bytes" flag, so a missing format (None) maps to b"" rather than a NULL
2031
+ ``c_char_p``.
2032
+ """
2033
+ return fmt if fmt is not None else b""
2034
+
2035
+
2036
+ def _is_read_stream(obj) -> bool:
2037
+ """Return True if obj is a stream-like object this SDK can use.
2038
+ Note: only method presence to identify streams are checked,
2039
+ not that they work (a broken stream can fail later).
2040
+ """
2041
+ if obj is None or isinstance(obj, (str, Path)):
2042
+ return False
2043
+ return all(hasattr(obj, method) for method in Stream._REQUIRED_STREAM_METHODS)
2044
+
2045
+
1998
2046
  class Reader(ManagedResource):
1999
2047
  """High-level wrapper for C2PA Reader operations.
2000
2048
 
@@ -2043,6 +2091,10 @@ class Reader(ManagedResource):
2043
2091
  def _is_mime_type_supported(cls, mime_type: str) -> bool:
2044
2092
  """Check if a MIME type is supported.
2045
2093
 
2094
+ Not used internally: the native library is the authority on format
2095
+ support and detects the type from the bytes. Kept for callers that
2096
+ want a pre-flight check before handing an asset to a Reader.
2097
+
2046
2098
  Args:
2047
2099
  mime_type: The MIME type to check
2048
2100
 
@@ -2057,7 +2109,16 @@ class Reader(ManagedResource):
2057
2109
  @overload
2058
2110
  def try_create(
2059
2111
  cls,
2060
- format_or_path: Union[str, Path],
2112
+ stream: Any,
2113
+ manifest_data: Optional[Any] = None,
2114
+ context: Optional['ContextProvider'] = None,
2115
+ ) -> Optional["Reader"]: ...
2116
+
2117
+ @classmethod
2118
+ @overload
2119
+ def try_create(
2120
+ cls,
2121
+ format_or_path: Union[str, Path, None] = None,
2061
2122
  stream: Optional[Any] = None,
2062
2123
  manifest_data: Optional[Any] = None,
2063
2124
  ) -> Optional["Reader"]: ...
@@ -2066,7 +2127,7 @@ class Reader(ManagedResource):
2066
2127
  @overload
2067
2128
  def try_create(
2068
2129
  cls,
2069
- format_or_path: Union[str, Path],
2130
+ format_or_path: Union[str, Path, None],
2070
2131
  stream: Optional[Any],
2071
2132
  manifest_data: Optional[Any],
2072
2133
  context: 'ContextProvider',
@@ -2075,7 +2136,7 @@ class Reader(ManagedResource):
2075
2136
  @classmethod
2076
2137
  def try_create(
2077
2138
  cls,
2078
- format_or_path: Union[str, Path],
2139
+ format_or_path: Union[str, Path, None] = None,
2079
2140
  stream: Optional[Any] = None,
2080
2141
  manifest_data: Optional[Any] = None,
2081
2142
  context: Optional['ContextProvider'] = None,
@@ -2089,8 +2150,13 @@ class Reader(ManagedResource):
2089
2150
  want to check if an asset contains C2PA data without handling
2090
2151
  exceptions for the expected case of no manifest.
2091
2152
 
2153
+ Pass a stream as the only argument (``try_create(stream)``) to read it
2154
+ with an auto-detected format.
2155
+
2092
2156
  Args:
2093
- format_or_path: The format or path to read from
2157
+ format_or_path: The format or path to read from, or a stream to
2158
+ read with an auto-detected format. None or an empty string
2159
+ requests auto-detection (best effort).
2094
2160
  stream: Optional stream to read from (Python stream-like object)
2095
2161
  manifest_data: Optional manifest data in bytes
2096
2162
  context: Optional ContextProvider for settings
@@ -2113,7 +2179,15 @@ class Reader(ManagedResource):
2113
2179
  @overload
2114
2180
  def __init__(
2115
2181
  self,
2116
- format_or_path: Union[str, Path],
2182
+ stream: Any,
2183
+ manifest_data: Optional[Any] = None,
2184
+ context: Optional['ContextProvider'] = None,
2185
+ ) -> None: ...
2186
+
2187
+ @overload
2188
+ def __init__(
2189
+ self,
2190
+ format_or_path: Union[str, Path, None] = None,
2117
2191
  stream: Optional[Any] = None,
2118
2192
  manifest_data: Optional[Any] = None,
2119
2193
  ) -> None: ...
@@ -2121,7 +2195,7 @@ class Reader(ManagedResource):
2121
2195
  @overload
2122
2196
  def __init__(
2123
2197
  self,
2124
- format_or_path: Union[str, Path],
2198
+ format_or_path: Union[str, Path, None],
2125
2199
  stream: Optional[Any],
2126
2200
  manifest_data: Optional[Any],
2127
2201
  context: 'ContextProvider',
@@ -2129,21 +2203,40 @@ class Reader(ManagedResource):
2129
2203
 
2130
2204
  def __init__(
2131
2205
  self,
2132
- format_or_path: Union[str, Path],
2206
+ format_or_path: Union[str, Path, None] = None,
2133
2207
  stream: Optional[Any] = None,
2134
2208
  manifest_data: Optional[Any] = None,
2135
2209
  context: Optional['ContextProvider'] = None,
2136
2210
  ):
2137
2211
  """Create a new Reader.
2138
2212
 
2213
+ The format is optional. Passing a known format gives the native
2214
+ library more to work with, so prefer it. Pass None, an empty string,
2215
+ or an extensionless file path to let the library detect the type from
2216
+ the asset bytes.
2217
+
2218
+ The library reconciles the format against the container it detects in
2219
+ the bytes: when the given format disagrees with the detected container,
2220
+ the library ignores the format and uses its own best guess (so reading
2221
+ does not fail on a wrong file extension); when no container is
2222
+ detected, the format is used as a hint and the asset is treated as a
2223
+ sidecar. If the bytes are not a recognized asset and no format resolves
2224
+ the type, a C2paError is raised.
2225
+
2226
+ Pass a stream as the only argument (``Reader(stream)``) to read it with
2227
+ an auto-detected format.
2228
+
2139
2229
  Args:
2140
- format_or_path: The format or path to read from
2230
+ format_or_path: The format (MIME type) or path to read from, or a
2231
+ stream to read with an auto-detected format. None or an empty
2232
+ string requests detection from the bytes.
2141
2233
  stream: Optional stream to read from (Python stream-like object)
2142
2234
  manifest_data: Optional manifest data in bytes
2143
2235
  context: Optional context implementing ContextProvider with settings
2144
2236
 
2145
2237
  Raises:
2146
- C2paError: If there was an error creating the reader
2238
+ C2paError: If there was an error creating the reader, including
2239
+ when the format cannot be detected from an unrecognized asset
2147
2240
  C2paError.Encoding: If any of the string inputs
2148
2241
  contain invalid UTF-8 characters
2149
2242
  """
@@ -2165,6 +2258,15 @@ class Reader(ManagedResource):
2165
2258
 
2166
2259
  self._context = context
2167
2260
 
2261
+ # Only stream, no format: Reader(fh) must auto-detect on the stream.
2262
+ if stream is None and _is_read_stream(format_or_path):
2263
+ stream = format_or_path
2264
+ format_or_path = None
2265
+ # A context supplies settings, not the asset, so a path or stream is
2266
+ # still required to read from.
2267
+ if format_or_path is None and stream is None:
2268
+ raise C2paError("Reader requires a path or a stream")
2269
+
2168
2270
  if context is not None:
2169
2271
  self._init_from_context(
2170
2272
  context, format_or_path, stream,
@@ -2172,50 +2274,53 @@ class Reader(ManagedResource):
2172
2274
  )
2173
2275
  return
2174
2276
 
2175
- supported = Reader.get_supported_mime_types()
2277
+ format_bytes = self._resolve_format_bytes(format_or_path, stream)
2176
2278
 
2177
2279
  if stream is None:
2178
- # Create a stream from the file path in format_or_path
2179
- path = str(format_or_path)
2180
- mime_type = _get_mime_type_from_path(path)
2181
-
2182
- if not mime_type:
2183
- raise C2paError.NotSupported(
2184
- f"Could not determine MIME type for file: {path}")
2185
-
2186
- format_bytes = _validate_and_encode_format(
2187
- mime_type, supported, "Reader")
2188
- self._init_from_file(path, format_bytes)
2280
+ # Create a stream from the file path in format_or_path.
2281
+ # A None format lets the native lib guess from the bytes.
2282
+ self._init_from_file(str(format_or_path), format_bytes)
2189
2283
 
2190
2284
  elif isinstance(stream, str):
2191
2285
  # stream is a file path, format_or_path is the format
2192
- format_bytes = _validate_and_encode_format(
2193
- str(format_or_path), supported, "Reader")
2194
2286
  self._init_from_file(
2195
2287
  stream, format_bytes, manifest_data)
2196
2288
 
2197
2289
  else:
2198
2290
  # format_or_path is a format string, stream is a stream object
2199
- format_bytes = _validate_and_encode_format(
2200
- str(format_or_path), supported, "Reader")
2201
-
2202
2291
  with Stream(stream) as stream_obj:
2203
2292
  self._create_reader(
2204
2293
  format_bytes, stream_obj, manifest_data)
2205
2294
  self._lifecycle_state = LifecycleState.ACTIVE
2206
2295
 
2296
+ @staticmethod
2297
+ def _resolve_format_bytes(format_or_path, stream) -> Optional[bytes]:
2298
+ """Resolve the encoded format for a (format_or_path, stream) pair.
2299
+
2300
+ When only a path is given (``stream is None``), the format is derived
2301
+ from the file extension. Otherwise ``format_or_path`` is the format
2302
+ string (or None).
2303
+ Returns None to request auto-detection.
2304
+ """
2305
+ if stream is None:
2306
+ return _encode_format(
2307
+ _get_mime_type_from_path(str(format_or_path)), "Reader")
2308
+ return _encode_format(
2309
+ None if format_or_path is None else str(format_or_path), "Reader")
2310
+
2207
2311
  def _create_reader(self, format_bytes, stream_obj,
2208
2312
  manifest_data=None):
2209
2313
  """Create a Reader from a Stream.
2210
2314
 
2211
2315
  Args:
2212
- format_bytes: UTF-8 encoded format/MIME type
2316
+ format_bytes: Encoded format/MIME type, or None for auto-detection
2213
2317
  stream_obj: A Stream instance
2214
2318
  manifest_data: Optional manifest bytes
2215
2319
  """
2320
+ format_arg = _format_ffi_arg(format_bytes)
2216
2321
  if manifest_data is None:
2217
2322
  self._handle = _lib.c2pa_reader_from_stream(
2218
- format_bytes, stream_obj._stream)
2323
+ format_arg, stream_obj._stream)
2219
2324
  else:
2220
2325
  if not isinstance(manifest_data, bytes):
2221
2326
  raise TypeError(Reader._ERROR_MESSAGES['manifest_error'])
@@ -2224,7 +2329,7 @@ class Reader(ManagedResource):
2224
2329
  len(manifest_data)).from_buffer_copy(manifest_data)
2225
2330
  self._handle = (
2226
2331
  _lib.c2pa_reader_from_manifest_data_and_stream(
2227
- format_bytes,
2332
+ format_arg,
2228
2333
  stream_obj._stream,
2229
2334
  manifest_array,
2230
2335
  len(manifest_data),
@@ -2269,26 +2374,16 @@ class Reader(ManagedResource):
2269
2374
  raise TypeError(Reader._ERROR_MESSAGES['manifest_error'])
2270
2375
 
2271
2376
  # Determine format and open stream
2272
- supported = Reader.get_supported_mime_types()
2273
-
2377
+ format_bytes = self._resolve_format_bytes(format_or_path, stream)
2378
+ format_arg = _format_ffi_arg(format_bytes)
2274
2379
  if stream is None:
2275
- path = str(format_or_path)
2276
- mime_type = _get_mime_type_from_path(path)
2277
- if not mime_type:
2278
- raise C2paError.NotSupported(
2279
- f"Could not determine MIME type for file: {path}")
2280
- format_bytes = _validate_and_encode_format(
2281
- mime_type, supported, "Reader")
2282
- self._backing_file = open(path, 'rb')
2380
+ # A None format lets the native lib guess from the bytes.
2381
+ self._backing_file = open(str(format_or_path), 'rb')
2283
2382
  self._own_stream = Stream(self._backing_file)
2284
2383
  elif isinstance(stream, str):
2285
- format_bytes = _validate_and_encode_format(
2286
- str(format_or_path), supported, "Reader")
2287
2384
  self._backing_file = open(stream, 'rb')
2288
2385
  self._own_stream = Stream(self._backing_file)
2289
2386
  else:
2290
- format_bytes = _validate_and_encode_format(
2291
- str(format_or_path), supported, "Reader")
2292
2387
  self._own_stream = Stream(stream)
2293
2388
 
2294
2389
  try:
@@ -2317,7 +2412,7 @@ class Reader(ManagedResource):
2317
2412
  new_ptr = (
2318
2413
  _lib.c2pa_reader_with_manifest_data_and_stream(
2319
2414
  reader_ptr,
2320
- format_bytes,
2415
+ format_arg,
2321
2416
  self._own_stream._stream,
2322
2417
  manifest_array,
2323
2418
  len(manifest_data),
@@ -2326,7 +2421,7 @@ class Reader(ManagedResource):
2326
2421
  else:
2327
2422
  # Consume reader with stream
2328
2423
  new_ptr = _lib.c2pa_reader_with_stream(
2329
- reader_ptr, format_bytes,
2424
+ reader_ptr, format_arg,
2330
2425
  self._own_stream._stream,
2331
2426
  )
2332
2427
 
@@ -2394,7 +2489,7 @@ class Reader(ManagedResource):
2394
2489
 
2395
2490
  return self._manifest_data_cache
2396
2491
 
2397
- def with_fragment(self, format: str, stream,
2492
+ def with_fragment(self, format: Optional[str], stream,
2398
2493
  fragment_stream) -> "Reader":
2399
2494
  """Process a BMFF fragment stream with this reader.
2400
2495
 
@@ -2402,7 +2497,9 @@ class Reader(ManagedResource):
2402
2497
  content is split into init segments and fragment files.
2403
2498
 
2404
2499
  Args:
2405
- format: MIME type of the media (e.g., "video/mp4")
2500
+ format: MIME type of the media (e.g., "video/mp4"). None or an
2501
+ empty string requests detection from the bytes; the native
2502
+ library reconciles the format against the detected container.
2406
2503
  stream: Stream-like object with the main/init segment data
2407
2504
  fragment_stream: Stream-like object with the fragment data
2408
2505
 
@@ -2414,15 +2511,12 @@ class Reader(ManagedResource):
2414
2511
  """
2415
2512
  self._ensure_valid_state()
2416
2513
 
2417
- supported = Reader.get_supported_mime_types()
2418
- format_bytes = _validate_and_encode_format(
2419
- format, supported, "Reader"
2420
- )
2514
+ format_arg = _format_ffi_arg(_encode_format(format, "Reader"))
2421
2515
 
2422
2516
  with Stream(stream) as main_obj, Stream(fragment_stream) as frag_obj:
2423
2517
  new_ptr = _lib.c2pa_reader_with_fragment(
2424
2518
  self._handle,
2425
- format_bytes,
2519
+ format_arg,
2426
2520
  main_obj._stream,
2427
2521
  frag_obj._stream,
2428
2522
  )
@@ -3338,7 +3432,8 @@ class Builder(ManagedResource):
3338
3432
  result = _lib.c2pa_builder_write_ingredient_archive(
3339
3433
  self._handle, ingredient_id_str, stream_obj._stream)
3340
3434
 
3341
- _check_ffi_operation_result(result,
3435
+ _check_ffi_operation_result(
3436
+ result,
3342
3437
  Builder._ERROR_MESSAGES["archive_error"].format(
3343
3438
  "Unknown error"
3344
3439
  ),
@@ -3361,7 +3456,8 @@ class Builder(ManagedResource):
3361
3456
  result = _lib.c2pa_builder_add_ingredient_from_archive(
3362
3457
  self._handle, stream_obj._stream)
3363
3458
 
3364
- _check_ffi_operation_result(result,
3459
+ _check_ffi_operation_result(
3460
+ result,
3365
3461
  Builder._ERROR_MESSAGES["archive_read_error"].format(
3366
3462
  "Unknown error"
3367
3463
  ),
@@ -3414,7 +3510,8 @@ class Builder(ManagedResource):
3414
3510
  are single-sign use. The Builder is closed after signing.
3415
3511
 
3416
3512
  Args:
3417
- format: The MIME type or extension of the content
3513
+ format: The MIME type or extension of the content. Required for
3514
+ signing: a missing format raises C2paError.NotSupported.
3418
3515
  source_stream: The source stream
3419
3516
  dest_stream: The destination stream,
3420
3517
  opened in w+b (write+read binary) mode.
@@ -3433,15 +3530,16 @@ class Builder(ManagedResource):
3433
3530
  if not hasattr(signer, '_handle') or not signer._handle:
3434
3531
  raise C2paError("Invalid or closed signer")
3435
3532
 
3436
- format_bytes = _validate_and_encode_format(
3437
- format, Builder.get_supported_mime_types(), "Builder")
3533
+ # allow_autodetect=False, so this never returns None (raises instead).
3534
+ format_arg = _format_ffi_arg(
3535
+ _encode_format(format, "Builder", allow_autodetect=False))
3438
3536
  manifest_bytes_ptr = ctypes.POINTER(ctypes.c_ubyte)()
3439
3537
 
3440
3538
  try:
3441
3539
  if signer is not None:
3442
3540
  result = _lib.c2pa_builder_sign(
3443
3541
  self._handle,
3444
- format_bytes,
3542
+ format_arg,
3445
3543
  source_stream._stream,
3446
3544
  dest_stream._stream,
3447
3545
  signer._handle,
@@ -3450,7 +3548,7 @@ class Builder(ManagedResource):
3450
3548
  else:
3451
3549
  result = _lib.c2pa_builder_sign_context(
3452
3550
  self._handle,
3453
- format_bytes,
3551
+ format_arg,
3454
3552
  source_stream._stream,
3455
3553
  dest_stream._stream,
3456
3554
  ctypes.byref(manifest_bytes_ptr),
@@ -3647,9 +3745,17 @@ class Builder(ManagedResource):
3647
3745
  Manifest bytes
3648
3746
 
3649
3747
  Raises:
3748
+ C2paError.NotSupported: If the format cannot be determined from
3749
+ the source path (extensionless or unrecognized extension),
3750
+ since signing requires an explicit, resolvable format.
3650
3751
  C2paError: If there was an error during signing
3651
3752
  """
3652
3753
  mime_type = _get_mime_type_from_path(source_path)
3754
+ if not mime_type:
3755
+ raise C2paError.NotSupported(
3756
+ "Could not determine the format (MIME type) from "
3757
+ f"'{source_path}'. Sign a stream with an explicit format "
3758
+ "instead, or use a recognized file extension.")
3653
3759
 
3654
3760
  try:
3655
3761
  with (
@@ -3660,6 +3766,9 @@ class Builder(ManagedResource):
3660
3766
  return self.sign(signer, mime_type, source_file, dest_file)
3661
3767
  # else:
3662
3768
  return self.sign(mime_type, source_file, dest_file)
3769
+ except C2paError:
3770
+ # Preserve C2paError and its subtypes
3771
+ raise
3663
3772
  except Exception as e:
3664
3773
  raise C2paError(f"Error signing file: {str(e)}") from e
3665
3774
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.37.1
3
+ Version: 0.37.2
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>, Tania Mathern <mathern@adobe.com>