Skip to content

Commit f36e35e

Browse files
authored
Merge branch 'mathern/context' into mathern/manifest-data-context
2 parents 8ad3673 + ebf6cdc commit f36e35e

5 files changed

Lines changed: 99 additions & 362 deletions

File tree

src/c2pa/c2pa.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
'c2pa_reader_from_context',
7878
'c2pa_reader_with_stream',
7979
'c2pa_reader_with_manifest_data_and_stream',
80+
'c2pa_reader_with_fragment',
8081
'c2pa_builder_from_context',
8182
'c2pa_builder_with_definition',
8283
'c2pa_builder_with_archive',
@@ -702,6 +703,12 @@ def _setup_function(func, argtypes, restype=None):
702703
ctypes.POINTER(ctypes.c_ubyte), ctypes.c_size_t],
703704
ctypes.POINTER(C2paReader),
704705
)
706+
_setup_function(
707+
_lib.c2pa_reader_with_fragment,
708+
[ctypes.POINTER(C2paReader), ctypes.c_char_p,
709+
ctypes.POINTER(C2paStream), ctypes.POINTER(C2paStream)],
710+
ctypes.POINTER(C2paReader)
711+
)
705712
_setup_function(
706713
_lib.c2pa_builder_from_context,
707714
[ctypes.POINTER(C2paContext)],
@@ -2100,7 +2107,8 @@ class Reader(ManagedResource):
21002107
'file_error': "Error cleaning up file: {}",
21012108
'reader_cleanup_error': "Error cleaning up reader: {}",
21022109
'encoding_error': "Invalid UTF-8 characters in input: {}",
2103-
'closed_error': "Reader is closed"
2110+
'closed_error': "Reader is closed",
2111+
'fragment_error': "Failed to process fragment: {}"
21042112
}
21052113

21062114
@classmethod
@@ -2499,6 +2507,58 @@ def _get_cached_manifest_data(self) -> Optional[dict]:
24992507

25002508
return self._manifest_data_cache
25012509

2510+
def with_fragment(self, format: str, stream,
2511+
fragment_stream) -> "Reader":
2512+
"""Process a BMFF fragment stream with this reader.
2513+
2514+
Used for fragmented BMFF media (DASH/HLS streaming) where
2515+
content is split into init segments and fragment files.
2516+
2517+
Args:
2518+
format: MIME type of the media (e.g., "video/mp4")
2519+
stream: Stream-like object with the main/init segment data
2520+
fragment_stream: Stream-like object with the fragment data
2521+
2522+
Returns:
2523+
This reader instance, for method chaining.
2524+
2525+
Raises:
2526+
C2paError: If there was an error processing the fragment
2527+
"""
2528+
self._ensure_valid_state()
2529+
2530+
supported = Reader.get_supported_mime_types()
2531+
format_bytes = _validate_and_encode_format(
2532+
format, supported, "Reader"
2533+
)
2534+
2535+
with Stream(stream) as main_obj, Stream(fragment_stream) as frag_obj:
2536+
new_ptr = _lib.c2pa_reader_with_fragment(
2537+
self._handle,
2538+
format_bytes,
2539+
main_obj._stream,
2540+
frag_obj._stream,
2541+
)
2542+
2543+
if not new_ptr:
2544+
self._handle = None
2545+
error = _parse_operation_result_for_error(
2546+
_lib.c2pa_error()
2547+
)
2548+
if error:
2549+
raise C2paError(error)
2550+
raise C2paError(
2551+
Reader._ERROR_MESSAGES[
2552+
'fragment_error'
2553+
].format("Unknown error"))
2554+
self._handle = new_ptr
2555+
2556+
# Invalidate caches: fragment may change manifest data
2557+
self._manifest_json_str_cache = None
2558+
self._manifest_data_cache = None
2559+
2560+
return self
2561+
25022562
def close(self):
25032563
"""Release the reader resources."""
25042564
self._manifest_json_str_cache = None

tests/fixtures/dash1.m4s

69.4 KB
Binary file not shown.

tests/fixtures/dashinit.mp4

4.65 KB
Binary file not shown.

tests/test_unit_tests.py

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def test_stream_read_detailed_and_parse(self):
256256
title = manifest_store["manifests"][manifest_store["active_manifest"]]["claim"]["dc:title"]
257257
self.assertEqual(title, DEFAULT_TEST_FILE_NAME)
258258

259-
def test_stream_read_string_stream(self):
259+
def test_stream_read_string_stream_path_only(self):
260260
with Reader(self.testPath) as reader:
261261
json_data = reader.json()
262262
self.assertIn(DEFAULT_TEST_FILE_NAME, json_data)
@@ -4666,7 +4666,7 @@ def test_builder_add_ingredient_from_file_path(self):
46664666

46674667
builder.close()
46684668

4669-
def test_builder_add_ingredient_from_file_path(self):
4669+
def test_builder_add_ingredient_from_file_path_not_found(self):
46704670
"""Test Builder class add_ingredient_from_file_path method."""
46714671

46724672
# Suppress the specific deprecation warning for this test, as this is a legacy method
@@ -4925,56 +4925,6 @@ def test_sign_file_callback_signer(self):
49254925
finally:
49264926
shutil.rmtree(temp_dir)
49274927

4928-
def test_sign_file_callback_signer(self):
4929-
"""Test signing a file using the sign_file method."""
4930-
4931-
temp_dir = tempfile.mkdtemp()
4932-
4933-
try:
4934-
output_path = os.path.join(temp_dir, "signed_output.jpg")
4935-
4936-
# Use the sign_file method
4937-
builder = Builder(self.manifestDefinition)
4938-
4939-
# Create signer with callback using create_signer function
4940-
signer = create_signer(
4941-
callback=self.callback_signer_es256,
4942-
alg=SigningAlg.ES256,
4943-
certs=self.certs.decode('utf-8'),
4944-
tsa_url="http://timestamp.digicert.com"
4945-
)
4946-
4947-
manifest_bytes = builder.sign_file(
4948-
source_path=self.testPath,
4949-
dest_path=output_path,
4950-
signer=signer
4951-
)
4952-
4953-
# Verify the output file was created
4954-
self.assertTrue(os.path.exists(output_path))
4955-
4956-
# Verify results
4957-
self.assertIsInstance(manifest_bytes, bytes)
4958-
self.assertGreater(len(manifest_bytes), 0)
4959-
4960-
# Read the signed file and verify the manifest
4961-
with open(output_path, "rb") as file, Reader("image/jpeg", file) as reader:
4962-
json_data = reader.json()
4963-
# Needs trust configuration to be set up to validate as Trusted
4964-
# self.assertNotIn("validation_status", json_data)
4965-
4966-
# Parse the JSON and verify the signature algorithm
4967-
manifest_data = json.loads(json_data)
4968-
active_manifest_id = manifest_data["active_manifest"]
4969-
active_manifest = manifest_data["manifests"][active_manifest_id]
4970-
4971-
self.assertIn("signature_info", active_manifest)
4972-
signature_info = active_manifest["signature_info"]
4973-
self.assertEqual(signature_info["alg"], self.callback_signer_alg)
4974-
4975-
finally:
4976-
shutil.rmtree(temp_dir)
4977-
49784928
def test_sign_file_callback_signer_managed_single(self):
49794929
"""Test signing a file using the sign_file method with context managers."""
49804930

@@ -5490,6 +5440,42 @@ def test_reader_format_and_path_with_ctx(self):
54905440
reader.close()
54915441
context.close()
54925442

5443+
def test_with_fragment_on_closed_reader_raises(self):
5444+
context = Context()
5445+
reader = Reader(DEFAULT_TEST_FILE, context=context)
5446+
reader.close()
5447+
with self.assertRaises(Error):
5448+
reader.with_fragment(
5449+
"video/mp4",
5450+
io.BytesIO(b"\x00" * 100),
5451+
io.BytesIO(b"\x00" * 100),
5452+
)
5453+
context.close()
5454+
5455+
def test_with_fragment_unsupported_format_raises(self):
5456+
context = Context()
5457+
reader = Reader(DEFAULT_TEST_FILE, context=context)
5458+
with self.assertRaises(Error):
5459+
reader.with_fragment(
5460+
"text/plain",
5461+
io.BytesIO(b"\x00" * 100),
5462+
io.BytesIO(b"\x00" * 100),
5463+
)
5464+
reader.close()
5465+
context.close()
5466+
5467+
def test_with_fragment_with_dash_fixtures(self):
5468+
context = Context()
5469+
init_path = os.path.join(FIXTURES_DIR, "dashinit.mp4")
5470+
with open(init_path, "rb") as init_fragment:
5471+
reader = Reader("video/mp4", init_fragment, context=context)
5472+
frag_path = os.path.join(FIXTURES_DIR, "dash1.m4s")
5473+
with open(init_path, "rb") as init_fragment, \
5474+
open(frag_path, "rb") as next_fragment:
5475+
reader.with_fragment("video/mp4", init_fragment, next_fragment)
5476+
reader.close()
5477+
context.close()
5478+
54935479

54945480
class TestBuilderWithContext(TestContextAPIs):
54955481

0 commit comments

Comments
 (0)