c2pa-python 0.29.0__tar.gz → 0.30.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.29.0/src/c2pa_python.egg-info → c2pa_python-0.30.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.29.0 → c2pa_python-0.30.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  4. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/tests/test_unit_tests.py +480 -1
  5. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/LICENSE-APACHE +0 -0
  6. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/LICENSE-MIT +0 -0
  7. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/MANIFEST.in +0 -0
  8. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/README.md +0 -0
  9. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/requirements.txt +0 -0
  10. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/scripts/download_artifacts.py +0 -0
  11. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/setup.cfg +0 -0
  12. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/setup.py +0 -0
  13. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa/__init__.py +0 -0
  14. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa/build.py +0 -0
  15. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa/c2pa.py +0 -0
  16. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.29.0 → c2pa_python-0.30.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.29.0 → c2pa_python-0.30.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.29.0
3
+ Version: 0.30.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.29.0"
7
+ version = "0.30.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" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.29.0
3
+ Version: 0.30.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>
@@ -70,7 +70,7 @@ def load_test_settings_json():
70
70
  class TestC2paSdk(unittest.TestCase):
71
71
  def test_sdk_version(self):
72
72
  # This test verifies the native libraries used match the expected version.
73
- self.assertIn("0.78.4", sdk_version())
73
+ self.assertIn("0.78.6", sdk_version())
74
74
 
75
75
 
76
76
  class TestReader(unittest.TestCase):
@@ -1021,6 +1021,35 @@ class TestBuilderWithSigner(unittest.TestCase):
1021
1021
  return signature
1022
1022
  self.callback_signer_es256 = callback_signer_es256
1023
1023
 
1024
+ def _create_ingredient_archive(self, ingredient_json=None):
1025
+ """Helper: create an ingredient archive from a single ingredient."""
1026
+ if ingredient_json is None:
1027
+ ingredient_json = {"title": "photo.jpg", "relationship": "componentOf"}
1028
+ manifest = {
1029
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
1030
+ "assertions": [
1031
+ {
1032
+ "label": "c2pa.actions",
1033
+ "data": {
1034
+ "actions": [
1035
+ {
1036
+ "action": "c2pa.created",
1037
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
1038
+ }
1039
+ ]
1040
+ },
1041
+ }
1042
+ ],
1043
+ }
1044
+ builder = Builder.from_json(manifest)
1045
+ with open(self.testPath, "rb") as f:
1046
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
1047
+ archive = io.BytesIO()
1048
+ builder.to_archive(archive)
1049
+ builder.close()
1050
+ archive.seek(0)
1051
+ return archive
1052
+
1024
1053
  def test_can_retrieve_builder_supported_mimetypes(self):
1025
1054
  result1 = Builder.get_supported_mime_types()
1026
1055
  self.assertTrue(len(result1) > 0)
@@ -4151,6 +4180,456 @@ class TestBuilderWithSigner(unittest.TestCase):
4151
4180
  # Make sure settings are put back to the common test defaults
4152
4181
  load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4153
4182
 
4183
+ def test_link_archive_label_on_signing_builder_placed(self):
4184
+ """Label set on the signing builder's add_ingredient links an
4185
+ ingredient archive to a c2pa.placed action."""
4186
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4187
+
4188
+ archive = self._create_ingredient_archive()
4189
+
4190
+ manifest = {
4191
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4192
+ "assertions": [
4193
+ {
4194
+ "label": "c2pa.actions.v2",
4195
+ "data": {
4196
+ "actions": [
4197
+ {
4198
+ "action": "c2pa.placed",
4199
+ "parameters": {
4200
+ "ingredientIds": ["my-ingredient"]
4201
+ },
4202
+ }
4203
+ ]
4204
+ },
4205
+ }
4206
+ ],
4207
+ }
4208
+
4209
+ builder = Builder.from_json(manifest)
4210
+ builder.add_ingredient(
4211
+ {"title": "photo.jpg", "relationship": "componentOf", "label": "my-ingredient"},
4212
+ "application/c2pa",
4213
+ archive,
4214
+ )
4215
+
4216
+ with open(self.testPath, "rb") as src:
4217
+ output = io.BytesIO()
4218
+ builder.sign(self.signer, "image/jpeg", src, output)
4219
+ output.seek(0)
4220
+
4221
+ reader = Reader("image/jpeg", output)
4222
+ manifest_data = json.loads(reader.json())
4223
+ active = manifest_data["active_manifest"]
4224
+ assertions = manifest_data["manifests"][active]["assertions"]
4225
+
4226
+ placed_action = None
4227
+ for assertion in assertions:
4228
+ if assertion.get("label") == "c2pa.actions.v2":
4229
+ for action in assertion["data"]["actions"]:
4230
+ if action["action"] == "c2pa.placed":
4231
+ placed_action = action
4232
+ break
4233
+
4234
+ self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4235
+ self.assertIn("parameters", placed_action)
4236
+ self.assertIn("ingredients", placed_action["parameters"])
4237
+ self.assertEqual(len(placed_action["parameters"]["ingredients"]), 1)
4238
+ self.assertIn(
4239
+ "c2pa.ingredient.v3",
4240
+ placed_action["parameters"]["ingredients"][0]["url"],
4241
+ )
4242
+
4243
+ reader.close()
4244
+ output.close()
4245
+ archive.close()
4246
+ builder.close()
4247
+
4248
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4249
+
4250
+ def test_link_archive_label_on_signing_builder_opened(self):
4251
+ """Label set on the signing builder's add_ingredient links an
4252
+ ingredient archive to a c2pa.opened action."""
4253
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4254
+
4255
+ archive = self._create_ingredient_archive(
4256
+ {"title": "photo.jpg", "relationship": "parentOf"}
4257
+ )
4258
+
4259
+ manifest = {
4260
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4261
+ "assertions": [
4262
+ {
4263
+ "label": "c2pa.actions.v2",
4264
+ "data": {
4265
+ "actions": [
4266
+ {
4267
+ "action": "c2pa.opened",
4268
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4269
+ "parameters": {
4270
+ "ingredientIds": ["my-ingredient"]
4271
+ },
4272
+ }
4273
+ ]
4274
+ },
4275
+ }
4276
+ ],
4277
+ }
4278
+
4279
+ builder = Builder.from_json(manifest)
4280
+ builder.add_ingredient(
4281
+ {"title": "photo.jpg", "relationship": "parentOf", "label": "my-ingredient"},
4282
+ "application/c2pa",
4283
+ archive,
4284
+ )
4285
+
4286
+ with open(self.testPath, "rb") as src:
4287
+ output = io.BytesIO()
4288
+ builder.sign(self.signer, "image/jpeg", src, output)
4289
+ output.seek(0)
4290
+
4291
+ reader = Reader("image/jpeg", output)
4292
+ manifest_data = json.loads(reader.json())
4293
+ active = manifest_data["active_manifest"]
4294
+ assertions = manifest_data["manifests"][active]["assertions"]
4295
+
4296
+ opened_action = None
4297
+ for assertion in assertions:
4298
+ if assertion.get("label") == "c2pa.actions.v2":
4299
+ for action in assertion["data"]["actions"]:
4300
+ if action["action"] == "c2pa.opened":
4301
+ opened_action = action
4302
+ break
4303
+
4304
+ self.assertIsNotNone(opened_action, "c2pa.opened action not found")
4305
+ self.assertIn("parameters", opened_action)
4306
+ self.assertIn("ingredients", opened_action["parameters"])
4307
+ self.assertEqual(len(opened_action["parameters"]["ingredients"]), 1)
4308
+ self.assertIn(
4309
+ "c2pa.ingredient.v3",
4310
+ opened_action["parameters"]["ingredients"][0]["url"],
4311
+ )
4312
+
4313
+ reader.close()
4314
+ output.close()
4315
+ archive.close()
4316
+ builder.close()
4317
+
4318
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4319
+
4320
+ def test_link_archive_two_ingredients_labels(self):
4321
+ """Two ingredient archives linked to two different actions via
4322
+ distinct labels. Verifies no cross-linking."""
4323
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4324
+
4325
+ archive1 = self._create_ingredient_archive(
4326
+ {"title": "photo-placed.jpg", "relationship": "componentOf"}
4327
+ )
4328
+ archive2 = self._create_ingredient_archive(
4329
+ {"title": "photo-opened.jpg", "relationship": "parentOf"}
4330
+ )
4331
+
4332
+ manifest = {
4333
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4334
+ "assertions": [
4335
+ {
4336
+ "label": "c2pa.actions.v2",
4337
+ "data": {
4338
+ "actions": [
4339
+ {
4340
+ "action": "c2pa.placed",
4341
+ "parameters": {
4342
+ "ingredientIds": ["ingredient-for-placed"]
4343
+ },
4344
+ },
4345
+ {
4346
+ "action": "c2pa.opened",
4347
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4348
+ "parameters": {
4349
+ "ingredientIds": ["ingredient-for-opened"]
4350
+ },
4351
+ },
4352
+ ]
4353
+ },
4354
+ }
4355
+ ],
4356
+ }
4357
+
4358
+ builder = Builder.from_json(manifest)
4359
+ builder.add_ingredient(
4360
+ {"title": "photo-placed.jpg", "relationship": "componentOf", "label": "ingredient-for-placed"},
4361
+ "application/c2pa",
4362
+ archive1,
4363
+ )
4364
+ builder.add_ingredient(
4365
+ {"title": "photo-opened.jpg", "relationship": "parentOf", "label": "ingredient-for-opened"},
4366
+ "application/c2pa",
4367
+ archive2,
4368
+ )
4369
+
4370
+ with open(self.testPath, "rb") as src:
4371
+ output = io.BytesIO()
4372
+ builder.sign(self.signer, "image/jpeg", src, output)
4373
+ output.seek(0)
4374
+
4375
+ reader = Reader("image/jpeg", output)
4376
+ manifest_data = json.loads(reader.json())
4377
+ active = manifest_data["active_manifest"]
4378
+ assertions = manifest_data["manifests"][active]["assertions"]
4379
+
4380
+ placed_action = None
4381
+ opened_action = None
4382
+ for assertion in assertions:
4383
+ if assertion.get("label") == "c2pa.actions.v2":
4384
+ for action in assertion["data"]["actions"]:
4385
+ if action["action"] == "c2pa.placed":
4386
+ placed_action = action
4387
+ if action["action"] == "c2pa.opened":
4388
+ opened_action = action
4389
+
4390
+ self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4391
+ self.assertIsNotNone(opened_action, "c2pa.opened action not found")
4392
+
4393
+ self.assertIn("ingredients", placed_action["parameters"])
4394
+ self.assertEqual(len(placed_action["parameters"]["ingredients"]), 1)
4395
+ placed_url = placed_action["parameters"]["ingredients"][0]["url"]
4396
+
4397
+ self.assertIn("ingredients", opened_action["parameters"])
4398
+ self.assertEqual(len(opened_action["parameters"]["ingredients"]), 1)
4399
+ opened_url = opened_action["parameters"]["ingredients"][0]["url"]
4400
+
4401
+ # Each action should link to a different ingredient (no cross-linking)
4402
+ self.assertNotEqual(placed_url, opened_url,
4403
+ "Each action should link to a different ingredient")
4404
+
4405
+ reader.close()
4406
+ output.close()
4407
+ archive1.close()
4408
+ archive2.close()
4409
+ builder.close()
4410
+
4411
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4412
+
4413
+ def test_link_archive_multiple_ingredients_in_one_placed_action(self):
4414
+ """A single c2pa.placed action references two componentOf ingredients
4415
+ via ingredientIds with two labels."""
4416
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4417
+
4418
+ archive1 = self._create_ingredient_archive(
4419
+ {"title": "base-layer.jpg", "relationship": "componentOf"}
4420
+ )
4421
+ archive2 = self._create_ingredient_archive(
4422
+ {"title": "overlay-layer.jpg", "relationship": "componentOf"}
4423
+ )
4424
+
4425
+ manifest = {
4426
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4427
+ "assertions": [
4428
+ {
4429
+ "label": "c2pa.actions.v2",
4430
+ "data": {
4431
+ "actions": [
4432
+ {
4433
+ "action": "c2pa.placed",
4434
+ "parameters": {
4435
+ "ingredientIds": ["base-layer", "overlay-layer"]
4436
+ },
4437
+ }
4438
+ ]
4439
+ },
4440
+ }
4441
+ ],
4442
+ }
4443
+
4444
+ builder = Builder.from_json(manifest)
4445
+ builder.add_ingredient(
4446
+ {"title": "base-layer.jpg", "relationship": "componentOf", "label": "base-layer"},
4447
+ "application/c2pa",
4448
+ archive1,
4449
+ )
4450
+ builder.add_ingredient(
4451
+ {"title": "overlay-layer.jpg", "relationship": "componentOf", "label": "overlay-layer"},
4452
+ "application/c2pa",
4453
+ archive2,
4454
+ )
4455
+
4456
+ with open(self.testPath, "rb") as src:
4457
+ output = io.BytesIO()
4458
+ builder.sign(self.signer, "image/jpeg", src, output)
4459
+ output.seek(0)
4460
+
4461
+ reader = Reader("image/jpeg", output)
4462
+ manifest_data = json.loads(reader.json())
4463
+ active = manifest_data["active_manifest"]
4464
+ assertions = manifest_data["manifests"][active]["assertions"]
4465
+
4466
+ placed_action = None
4467
+ for assertion in assertions:
4468
+ if assertion.get("label") == "c2pa.actions.v2":
4469
+ for action in assertion["data"]["actions"]:
4470
+ if action["action"] == "c2pa.placed":
4471
+ placed_action = action
4472
+ break
4473
+
4474
+ self.assertIsNotNone(placed_action, "c2pa.placed action not found")
4475
+ self.assertIn("parameters", placed_action)
4476
+ self.assertIn("ingredients", placed_action["parameters"])
4477
+ ingredients = placed_action["parameters"]["ingredients"]
4478
+ self.assertEqual(len(ingredients), 2,
4479
+ "c2pa.placed should reference both ingredients")
4480
+
4481
+ url0 = ingredients[0]["url"]
4482
+ url1 = ingredients[1]["url"]
4483
+ self.assertNotEqual(url0, url1,
4484
+ "Each ingredient should have a distinct URL")
4485
+
4486
+ reader.close()
4487
+ output.close()
4488
+ archive1.close()
4489
+ archive2.close()
4490
+ builder.close()
4491
+
4492
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
4493
+
4494
+ def test_ingredient_fields_survive_archive(self):
4495
+ archive = self._create_ingredient_archive({
4496
+ "title": "tracked-asset.jpg",
4497
+ "relationship": "componentOf",
4498
+ "instance_id": "tracking:project-7:asset-42",
4499
+ "description": "A tracked ingredient",
4500
+ "informational_URI": "https://example.com/assets/42",
4501
+ })
4502
+
4503
+ reader = Reader("application/c2pa", archive)
4504
+ manifest_data = json.loads(reader.json())
4505
+ active = manifest_data["active_manifest"]
4506
+ ingredients = manifest_data["manifests"][active]["ingredients"]
4507
+
4508
+ self.assertGreaterEqual(len(ingredients), 1)
4509
+ ing = ingredients[0]
4510
+
4511
+ self.assertEqual(ing["title"], "tracked-asset.jpg")
4512
+ self.assertIn("instance_id", ing)
4513
+ self.assertEqual(ing["instance_id"], "tracking:project-7:asset-42")
4514
+
4515
+ reader.close()
4516
+ archive.close()
4517
+
4518
+ def test_ingredient_fields_survive_archive_then_sign(self):
4519
+ """instance_id set on the archive ingredient persists through
4520
+ archive then sign."""
4521
+ archive = self._create_ingredient_archive({
4522
+ "title": "tracked-asset.jpg",
4523
+ "relationship": "componentOf",
4524
+ "instance_id": "tracking:project-7:asset-42",
4525
+ "description": "A tracked ingredient",
4526
+ "informational_URI": "https://example.com/assets/42",
4527
+ })
4528
+
4529
+ manifest = {
4530
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4531
+ "assertions": [
4532
+ {
4533
+ "label": "c2pa.actions",
4534
+ "data": {
4535
+ "actions": [
4536
+ {
4537
+ "action": "c2pa.created",
4538
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4539
+ }
4540
+ ]
4541
+ },
4542
+ }
4543
+ ],
4544
+ }
4545
+
4546
+ builder = Builder.from_json(manifest)
4547
+ builder.add_ingredient(
4548
+ {"title": "tracked-asset.jpg", "relationship": "componentOf"},
4549
+ "application/c2pa",
4550
+ archive,
4551
+ )
4552
+
4553
+ with open(self.testPath, "rb") as src:
4554
+ output = io.BytesIO()
4555
+ builder.sign(self.signer, "image/jpeg", src, output)
4556
+ output.seek(0)
4557
+
4558
+ reader = Reader("image/jpeg", output)
4559
+ manifest_data = json.loads(reader.json())
4560
+ active = manifest_data["active_manifest"]
4561
+ ingredients = manifest_data["manifests"][active]["ingredients"]
4562
+
4563
+ self.assertGreaterEqual(len(ingredients), 1)
4564
+ ing = ingredients[0]
4565
+
4566
+ self.assertIn("instance_id", ing)
4567
+ self.assertEqual(ing["instance_id"], "tracking:project-7:asset-42")
4568
+
4569
+ reader.close()
4570
+ output.close()
4571
+ archive.close()
4572
+ builder.close()
4573
+
4574
+ def test_instance_id_as_ingredient_identifier_in_catalog(self):
4575
+ """Two ingredients with different instance_id values in one archive.
4576
+ Read the archive back and select an ingredient by instance_id."""
4577
+ manifest = {
4578
+ "claim_generator_info": [{"name": "c2pa-test", "version": "1.0"}],
4579
+ "assertions": [
4580
+ {
4581
+ "label": "c2pa.actions",
4582
+ "data": {
4583
+ "actions": [
4584
+ {
4585
+ "action": "c2pa.created",
4586
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation",
4587
+ }
4588
+ ]
4589
+ },
4590
+ }
4591
+ ],
4592
+ }
4593
+
4594
+ builder = Builder.from_json(manifest)
4595
+ with open(self.testPath, "rb") as f:
4596
+ builder.add_ingredient(
4597
+ {"title": "photo-A.jpg", "relationship": "componentOf",
4598
+ "instance_id": "catalog:photo-A"},
4599
+ "image/jpeg", f,
4600
+ )
4601
+ with open(self.testPath, "rb") as f:
4602
+ builder.add_ingredient(
4603
+ {"title": "photo-B.jpg", "relationship": "componentOf",
4604
+ "instance_id": "catalog:photo-B"},
4605
+ "image/jpeg", f,
4606
+ )
4607
+
4608
+ archive = io.BytesIO()
4609
+ builder.to_archive(archive)
4610
+ archive.seek(0)
4611
+ builder.close()
4612
+
4613
+ reader = Reader("application/c2pa", archive)
4614
+ manifest_data = json.loads(reader.json())
4615
+ active = manifest_data["active_manifest"]
4616
+ ingredients = manifest_data["manifests"][active]["ingredients"]
4617
+
4618
+ self.assertEqual(len(ingredients), 2)
4619
+
4620
+ found = None
4621
+ for ing in ingredients:
4622
+ if ing.get("instance_id") == "catalog:photo-B":
4623
+ found = ing
4624
+ break
4625
+
4626
+ self.assertIsNotNone(found,
4627
+ "Should find ingredient by instance_id 'catalog:photo-B' in archive")
4628
+ self.assertEqual(found["title"], "photo-B.jpg")
4629
+
4630
+ reader.close()
4631
+ archive.close()
4632
+
4154
4633
 
4155
4634
  class TestStream(unittest.TestCase):
4156
4635
  def setUp(self):
File without changes
File without changes
File without changes
File without changes
File without changes