c2pa-python 0.26.0__tar.gz → 0.27.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.26.0/src/c2pa_python.egg-info → c2pa_python-0.27.0}/PKG-INFO +1 -1
  2. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/pyproject.toml +1 -1
  3. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa/c2pa.py +83 -10
  4. {c2pa_python-0.26.0 → c2pa_python-0.27.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
  5. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/tests/test_unit_tests.py +629 -1
  6. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/LICENSE-APACHE +0 -0
  7. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/LICENSE-MIT +0 -0
  8. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/MANIFEST.in +0 -0
  9. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/README.md +0 -0
  10. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/requirements.txt +0 -0
  11. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/scripts/download_artifacts.py +0 -0
  12. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/setup.cfg +0 -0
  13. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/setup.py +0 -0
  14. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa/__init__.py +0 -0
  15. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa/build.py +0 -0
  16. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa/lib.py +0 -0
  17. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
  18. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
  19. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
  20. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/requires.txt +0 -0
  21. {c2pa_python-0.26.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
  22. {c2pa_python-0.26.0 → c2pa_python-0.27.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.26.0
3
+ Version: 0.27.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.26.0"
7
+ version = "0.27.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" }
@@ -49,6 +49,7 @@ _REQUIRED_FUNCTIONS = [
49
49
  'c2pa_builder_set_remote_url',
50
50
  'c2pa_builder_add_resource',
51
51
  'c2pa_builder_add_ingredient_from_stream',
52
+ 'c2pa_builder_add_action',
52
53
  'c2pa_builder_to_archive',
53
54
  'c2pa_builder_sign',
54
55
  'c2pa_manifest_bytes_free',
@@ -403,6 +404,9 @@ _setup_function(_lib.c2pa_builder_add_ingredient_from_stream,
403
404
  ctypes.c_char_p,
404
405
  ctypes.POINTER(C2paStream)],
405
406
  ctypes.c_int)
407
+ _setup_function(_lib.c2pa_builder_add_action,
408
+ [ctypes.POINTER(C2paBuilder), ctypes.c_char_p],
409
+ ctypes.c_int)
406
410
  _setup_function(_lib.c2pa_builder_to_archive,
407
411
  [ctypes.POINTER(C2paBuilder), ctypes.POINTER(C2paStream)],
408
412
  ctypes.c_int)
@@ -667,20 +671,44 @@ def version() -> str:
667
671
  return _convert_to_py_string(result)
668
672
 
669
673
 
674
+ @overload
670
675
  def load_settings(settings: str, format: str = "json") -> None:
671
- """Load C2PA settings from a string.
676
+ ...
677
+
678
+
679
+ @overload
680
+ def load_settings(settings: dict) -> None:
681
+ ...
682
+
683
+
684
+ def load_settings(settings: Union[str, dict], format: str = "json") -> None:
685
+ """Load C2PA settings from a string or dict.
672
686
 
673
687
  Args:
674
- settings: The settings string to load
675
- format: The format of the settings string (default: "json")
688
+ settings: The settings string or dict to load
689
+ format: The format of the settings string (default: "json").
690
+ Ignored when settings is a dict.
676
691
 
677
692
  Raises:
678
693
  C2paError: If there was an error loading the settings
679
694
  """
680
- result = _lib.c2pa_load_settings(
681
- settings.encode('utf-8'),
682
- format.encode('utf-8')
683
- )
695
+ # Convert to JSON string as necessary
696
+ try:
697
+ if isinstance(settings, dict):
698
+ settings_str = json.dumps(settings)
699
+ format = "json"
700
+ else:
701
+ settings_str = settings
702
+ except (TypeError, ValueError) as e:
703
+ raise C2paError(f"Failed to serialize settings to JSON: {e}")
704
+
705
+ try:
706
+ settings_bytes = settings_str.encode('utf-8')
707
+ format_bytes = format.encode('utf-8')
708
+ except (AttributeError, UnicodeEncodeError) as e:
709
+ raise C2paError(f"Failed to encode settings to UTF-8: {e}")
710
+
711
+ result = _lib.c2pa_load_settings(settings_bytes, format_bytes)
684
712
  if result != 0:
685
713
  error = _parse_operation_result_for_error(_lib.c2pa_error())
686
714
  if error:
@@ -2198,6 +2226,7 @@ class Builder:
2198
2226
  'url_error': "Error setting remote URL: {}",
2199
2227
  'resource_error': "Error adding resource: {}",
2200
2228
  'ingredient_error': "Error adding ingredient: {}",
2229
+ 'action_error': "Error adding action: {}",
2201
2230
  'archive_error': "Error writing archive: {}",
2202
2231
  'sign_error': "Error during signing: {}",
2203
2232
  'encoding_error': "Invalid UTF-8 characters in manifest: {}",
@@ -2495,13 +2524,16 @@ class Builder:
2495
2524
  )
2496
2525
  )
2497
2526
 
2498
- def add_ingredient(self, ingredient_json: str, format: str, source: Any):
2527
+ def add_ingredient(
2528
+ self, ingredient_json: Union[str, dict], format: str, source: Any
2529
+ ):
2499
2530
  """Add an ingredient to the builder (facade method).
2500
2531
  The added ingredient's source should be a stream-like object
2501
2532
  (for instance, a file opened as stream).
2502
2533
 
2503
2534
  Args:
2504
2535
  ingredient_json: The JSON ingredient definition
2536
+ (either a JSON string or a dictionary)
2505
2537
  format: The MIME type or extension of the ingredient
2506
2538
  source: The stream containing the ingredient data
2507
2539
  (any Python stream-like object)
@@ -2521,7 +2553,7 @@ class Builder:
2521
2553
 
2522
2554
  def add_ingredient_from_stream(
2523
2555
  self,
2524
- ingredient_json: str,
2556
+ ingredient_json: Union[str, dict],
2525
2557
  format: str,
2526
2558
  source: Any):
2527
2559
  """Add an ingredient from a stream to the builder.
@@ -2529,6 +2561,7 @@ class Builder:
2529
2561
 
2530
2562
  Args:
2531
2563
  ingredient_json: The JSON ingredient definition
2564
+ (either a JSON string or a dictionary)
2532
2565
  format: The MIME type or extension of the ingredient
2533
2566
  source: The stream containing the ingredient data
2534
2567
  (any Python stream-like object)
@@ -2540,6 +2573,9 @@ class Builder:
2540
2573
  """
2541
2574
  self._ensure_valid_state()
2542
2575
 
2576
+ if isinstance(ingredient_json, dict):
2577
+ ingredient_json = json.dumps(ingredient_json)
2578
+
2543
2579
  try:
2544
2580
  ingredient_str = ingredient_json.encode('utf-8')
2545
2581
  format_str = format.encode('utf-8')
@@ -2570,7 +2606,7 @@ class Builder:
2570
2606
 
2571
2607
  def add_ingredient_from_file_path(
2572
2608
  self,
2573
- ingredient_json: str,
2609
+ ingredient_json: Union[str, dict],
2574
2610
  format: str,
2575
2611
  filepath: Union[str, Path]):
2576
2612
  """Add an ingredient from a file path to the builder (deprecated).
@@ -2582,6 +2618,7 @@ class Builder:
2582
2618
 
2583
2619
  Args:
2584
2620
  ingredient_json: The JSON ingredient definition
2621
+ (either a JSON string or a dictionary)
2585
2622
  format: The MIME type or extension of the ingredient
2586
2623
  filepath: The path to the file containing the ingredient data
2587
2624
  (can be a string or Path object)
@@ -2613,6 +2650,42 @@ class Builder:
2613
2650
  except Exception as e:
2614
2651
  raise C2paError.Other(f"Could not add ingredient: {e}") from e
2615
2652
 
2653
+ def add_action(self, action_json: Union[str, dict]) -> None:
2654
+ """Add an action to the builder, that will be placed
2655
+ in the actions assertion array in the generated manifest.
2656
+
2657
+ Args:
2658
+ action_json: The JSON action definition
2659
+ (either a JSON string or a dictionary)
2660
+
2661
+ Raises:
2662
+ C2paError: If there was an error adding the action
2663
+ C2paError.Encoding: If the action JSON contains invalid UTF-8 chars
2664
+ """
2665
+ self._ensure_valid_state()
2666
+
2667
+ if isinstance(action_json, dict):
2668
+ action_json = json.dumps(action_json)
2669
+
2670
+ try:
2671
+ action_str = action_json.encode('utf-8')
2672
+ except UnicodeError as e:
2673
+ raise C2paError.Encoding(
2674
+ Builder._ERROR_MESSAGES['encoding_error'].format(str(e))
2675
+ )
2676
+
2677
+ result = _lib.c2pa_builder_add_action(self._builder, action_str)
2678
+
2679
+ if result != 0:
2680
+ error = _parse_operation_result_for_error(_lib.c2pa_error())
2681
+ if error:
2682
+ raise C2paError(error)
2683
+ raise C2paError(
2684
+ Builder._ERROR_MESSAGES['action_error'].format(
2685
+ "Unknown error"
2686
+ )
2687
+ )
2688
+
2616
2689
  def to_archive(self, stream: Any) -> None:
2617
2690
  """Write an archive of the builder to a stream.
2618
2691
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: c2pa-python
3
- Version: 0.26.0
3
+ Version: 0.27.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>
@@ -733,6 +733,7 @@ class TestBuilderWithSigner(unittest.TestCase):
733
733
  "actions": [
734
734
  {
735
735
  "action": "c2pa.opened"
736
+ # Should have more parameters here, but omitted in tests
736
737
  }
737
738
  ]
738
739
  }
@@ -742,7 +743,6 @@ class TestBuilderWithSigner(unittest.TestCase):
742
743
 
743
744
  # Define a V2 manifest as a dictionary
744
745
  self.manifestDefinitionV2 = {
745
- "claim_generator": "python_test",
746
746
  "claim_generator_info": [{
747
747
  "name": "python_test",
748
748
  "version": "0.0.1",
@@ -1289,6 +1289,17 @@ class TestBuilderWithSigner(unittest.TestCase):
1289
1289
 
1290
1290
  builder.close()
1291
1291
 
1292
+ def test_builder_add_ingredient_dict(self):
1293
+ builder = Builder.from_json(self.manifestDefinition)
1294
+ assert builder._builder is not None
1295
+
1296
+ # Test adding ingredient with a dictionary instead of JSON string
1297
+ ingredient_dict = {"test": "ingredient"}
1298
+ with open(self.testPath, 'rb') as f:
1299
+ builder.add_ingredient(ingredient_dict, "image/jpeg", f)
1300
+
1301
+ builder.close()
1302
+
1292
1303
  def test_builder_add_multiple_ingredients(self):
1293
1304
  builder = Builder.from_json(self.manifestDefinition)
1294
1305
  assert builder._builder is not None
@@ -1309,6 +1320,26 @@ class TestBuilderWithSigner(unittest.TestCase):
1309
1320
 
1310
1321
  builder.close()
1311
1322
 
1323
+ def test_builder_add_multiple_ingredients_2(self):
1324
+ builder = Builder.from_json(self.manifestDefinition)
1325
+ assert builder._builder is not None
1326
+
1327
+ # Test builder operations
1328
+ builder.set_no_embed()
1329
+ builder.set_remote_url("http://test.url")
1330
+
1331
+ # Test adding ingredient with a dictionary
1332
+ ingredient_dict = {"test": "ingredient"}
1333
+ with open(self.testPath, 'rb') as f:
1334
+ builder.add_ingredient(ingredient_dict, "image/jpeg", f)
1335
+
1336
+ # Test adding another ingredient with a JSON string
1337
+ ingredient_json = '{"test": "ingredient2"}'
1338
+ with open(self.testPath2, 'rb') as f:
1339
+ builder.add_ingredient(ingredient_json, "image/png", f)
1340
+
1341
+ builder.close()
1342
+
1312
1343
  def test_builder_add_multiple_ingredients_and_resources(self):
1313
1344
  builder = Builder.from_json(self.manifestDefinition)
1314
1345
  assert builder._builder is not None
@@ -1451,6 +1482,53 @@ class TestBuilderWithSigner(unittest.TestCase):
1451
1482
  # Settings are thread-local, so we reset to the default "true" here
1452
1483
  load_settings('{"builder": { "thumbnail": {"enabled": true}}}')
1453
1484
 
1485
+ def test_builder_sign_with_settingdict_no_thumbnail_and_ingredient(self):
1486
+ builder = Builder.from_json(self.manifestDefinition)
1487
+ assert builder._builder is not None
1488
+
1489
+ # The following removes the manifest's thumbnail - using dict instead of string
1490
+ load_settings({"builder": {"thumbnail": {"enabled": False}}})
1491
+
1492
+ # Test adding ingredient
1493
+ ingredient_json = '{ "title": "Test Ingredient" }'
1494
+ with open(self.testPath3, 'rb') as f:
1495
+ builder.add_ingredient(ingredient_json, "image/jpeg", f)
1496
+
1497
+ with open(self.testPath2, "rb") as file:
1498
+ output = io.BytesIO(bytearray())
1499
+ builder.sign(self.signer, "image/jpeg", file, output)
1500
+ output.seek(0)
1501
+ reader = Reader("image/jpeg", output)
1502
+ json_data = reader.json()
1503
+ manifest_data = json.loads(json_data)
1504
+
1505
+ # Verify active manifest exists
1506
+ self.assertIn("active_manifest", manifest_data)
1507
+ active_manifest_id = manifest_data["active_manifest"]
1508
+
1509
+ # Verify active manifest object exists
1510
+ self.assertIn("manifests", manifest_data)
1511
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
1512
+ active_manifest = manifest_data["manifests"][active_manifest_id]
1513
+
1514
+ # There should be no thumbnail anymore here
1515
+ self.assertNotIn("thumbnail", active_manifest)
1516
+
1517
+ # Verify ingredients array exists in active manifest
1518
+ self.assertIn("ingredients", active_manifest)
1519
+ self.assertIsInstance(active_manifest["ingredients"], list)
1520
+ self.assertTrue(len(active_manifest["ingredients"]) > 0)
1521
+
1522
+ # Verify the first ingredient's title matches what we set
1523
+ first_ingredient = active_manifest["ingredients"][0]
1524
+ self.assertEqual(first_ingredient["title"], "Test Ingredient")
1525
+ self.assertNotIn("thumbnail", first_ingredient)
1526
+
1527
+ builder.close()
1528
+
1529
+ # Settings are thread-local, so we reset to the default "true" here - using dict instead of string
1530
+ load_settings({"builder": {"thumbnail": {"enabled": True}}})
1531
+
1454
1532
  def test_builder_sign_with_duplicate_ingredient(self):
1455
1533
  builder = Builder.from_json(self.manifestDefinition)
1456
1534
  assert builder._builder is not None
@@ -1537,6 +1615,46 @@ class TestBuilderWithSigner(unittest.TestCase):
1537
1615
 
1538
1616
  builder.close()
1539
1617
 
1618
+ def test_builder_sign_with_ingredient_dict_from_stream(self):
1619
+ builder = Builder.from_json(self.manifestDefinition)
1620
+ assert builder._builder is not None
1621
+
1622
+ # Test adding ingredient using stream with a dictionary
1623
+ ingredient_dict = {"title": "Test Ingredient Stream"}
1624
+ with open(self.testPath3, 'rb') as f:
1625
+ builder.add_ingredient_from_stream(
1626
+ ingredient_dict, "image/jpeg", f)
1627
+
1628
+ with open(self.testPath2, "rb") as file:
1629
+ output = io.BytesIO(bytearray())
1630
+ builder.sign(self.signer, "image/jpeg", file, output)
1631
+ output.seek(0)
1632
+ reader = Reader("image/jpeg", output)
1633
+ json_data = reader.json()
1634
+ manifest_data = json.loads(json_data)
1635
+
1636
+ # Verify active manifest exists
1637
+ self.assertIn("active_manifest", manifest_data)
1638
+ active_manifest_id = manifest_data["active_manifest"]
1639
+
1640
+ # Verify active manifest object exists
1641
+ self.assertIn("manifests", manifest_data)
1642
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
1643
+ active_manifest = manifest_data["manifests"][active_manifest_id]
1644
+
1645
+ # Verify ingredients array exists in active manifest
1646
+ self.assertIn("ingredients", active_manifest)
1647
+ self.assertIsInstance(active_manifest["ingredients"], list)
1648
+ self.assertTrue(len(active_manifest["ingredients"]) > 0)
1649
+
1650
+ # Verify the first ingredient's title matches what we set
1651
+ first_ingredient = active_manifest["ingredients"][0]
1652
+ self.assertEqual(
1653
+ first_ingredient["title"],
1654
+ "Test Ingredient Stream")
1655
+
1656
+ builder.close()
1657
+
1540
1658
  def test_builder_sign_with_multiple_ingredient(self):
1541
1659
  builder = Builder.from_json(self.manifestDefinition)
1542
1660
  assert builder._builder is not None
@@ -2323,6 +2441,476 @@ class TestBuilderWithSigner(unittest.TestCase):
2323
2441
  with self.assertRaises(Error):
2324
2442
  builder.set_no_embed()
2325
2443
 
2444
+ def test_builder_add_action_to_manifest_no_auto_add(self):
2445
+ # For testing, remove auto-added actions
2446
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
2447
+
2448
+ initial_manifest_definition = {
2449
+ "claim_generator_info": [{
2450
+ "name": "python_test",
2451
+ "version": "0.0.1",
2452
+ }],
2453
+ # claim version 2 is the default
2454
+ # "claim_version": 2,
2455
+ "format": "image/jpeg",
2456
+ "title": "Python Test Image V2",
2457
+ "assertions": [
2458
+ {
2459
+ "label": "c2pa.actions",
2460
+ "data": {
2461
+ "actions": [
2462
+ {
2463
+ "action": "c2pa.created",
2464
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
2465
+ }
2466
+ ]
2467
+ }
2468
+ }
2469
+ ]
2470
+ }
2471
+ builder = Builder.from_json(initial_manifest_definition)
2472
+
2473
+ action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}'
2474
+ builder.add_action(action_json)
2475
+
2476
+ with open(self.testPath2, "rb") as file:
2477
+ output = io.BytesIO(bytearray())
2478
+ builder.sign(self.signer, "image/jpeg", file, output)
2479
+ output.seek(0)
2480
+ reader = Reader("image/jpeg", output)
2481
+ json_data = reader.json()
2482
+ manifest_data = json.loads(json_data)
2483
+
2484
+ # Verify active manifest exists
2485
+ self.assertIn("active_manifest", manifest_data)
2486
+ active_manifest_id = manifest_data["active_manifest"]
2487
+
2488
+ # Verify active manifest object exists
2489
+ self.assertIn("manifests", manifest_data)
2490
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2491
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2492
+
2493
+ # Verify assertions object exists in active manifest
2494
+ self.assertIn("assertions", active_manifest)
2495
+ assertions = active_manifest["assertions"]
2496
+
2497
+ # Find the c2pa.actions.v2 assertion to check what we added
2498
+ actions_assertion = None
2499
+ for assertion in assertions:
2500
+ if assertion.get("label") == "c2pa.actions.v2":
2501
+ actions_assertion = assertion
2502
+ break
2503
+
2504
+ self.assertIsNotNone(actions_assertion)
2505
+ self.assertIn("data", actions_assertion)
2506
+ assertion_data = actions_assertion["data"]
2507
+ # Verify the manifest now contains actions
2508
+ self.assertIn("actions", assertion_data)
2509
+ actions = assertion_data["actions"]
2510
+ # Verify "c2pa.color_adjustments" action exists anywhere in the actions array
2511
+ created_action_found = False
2512
+ for action in actions:
2513
+ if action.get("action") == "c2pa.color_adjustments":
2514
+ created_action_found = True
2515
+ break
2516
+
2517
+ self.assertTrue(created_action_found)
2518
+
2519
+ builder.close()
2520
+
2521
+ # Reset settings
2522
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2523
+
2524
+ def test_builder_add_action_to_manifest_from_dict_no_auto_add(self):
2525
+ # For testing, remove auto-added actions
2526
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
2527
+
2528
+ initial_manifest_definition = {
2529
+ "claim_generator_info": [{
2530
+ "name": "python_test",
2531
+ "version": "0.0.1",
2532
+ }],
2533
+ # claim version 2 is the default
2534
+ # "claim_version": 2,
2535
+ "format": "image/jpeg",
2536
+ "title": "Python Test Image V2",
2537
+ "assertions": [
2538
+ {
2539
+ "label": "c2pa.actions",
2540
+ "data": {
2541
+ "actions": [
2542
+ {
2543
+ "action": "c2pa.created",
2544
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
2545
+ }
2546
+ ]
2547
+ }
2548
+ }
2549
+ ]
2550
+ }
2551
+ builder = Builder.from_json(initial_manifest_definition)
2552
+
2553
+ # Using a dictionary instead of a JSON string
2554
+ action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}
2555
+ builder.add_action(action_dict)
2556
+
2557
+ with open(self.testPath2, "rb") as file:
2558
+ output = io.BytesIO(bytearray())
2559
+ builder.sign(self.signer, "image/jpeg", file, output)
2560
+ output.seek(0)
2561
+ reader = Reader("image/jpeg", output)
2562
+ json_data = reader.json()
2563
+ manifest_data = json.loads(json_data)
2564
+
2565
+ # Verify active manifest exists
2566
+ self.assertIn("active_manifest", manifest_data)
2567
+ active_manifest_id = manifest_data["active_manifest"]
2568
+
2569
+ # Verify active manifest object exists
2570
+ self.assertIn("manifests", manifest_data)
2571
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2572
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2573
+
2574
+ # Verify assertions object exists in active manifest
2575
+ self.assertIn("assertions", active_manifest)
2576
+ assertions = active_manifest["assertions"]
2577
+
2578
+ # Find the c2pa.actions.v2 assertion to check what we added
2579
+ actions_assertion = None
2580
+ for assertion in assertions:
2581
+ if assertion.get("label") == "c2pa.actions.v2":
2582
+ actions_assertion = assertion
2583
+ break
2584
+
2585
+ self.assertIsNotNone(actions_assertion)
2586
+ self.assertIn("data", actions_assertion)
2587
+ assertion_data = actions_assertion["data"]
2588
+ # Verify the manifest now contains actions
2589
+ self.assertIn("actions", assertion_data)
2590
+ actions = assertion_data["actions"]
2591
+ # Verify "c2pa.color_adjustments" action exists anywhere in the actions array
2592
+ created_action_found = False
2593
+ for action in actions:
2594
+ if action.get("action") == "c2pa.color_adjustments":
2595
+ created_action_found = True
2596
+ break
2597
+
2598
+ self.assertTrue(created_action_found)
2599
+
2600
+ builder.close()
2601
+
2602
+ # Reset settings
2603
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2604
+
2605
+ def test_builder_add_action_to_manifest_with_auto_add(self):
2606
+ # For testing, force settings
2607
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2608
+
2609
+ initial_manifest_definition = {
2610
+ "claim_generator_info": [{
2611
+ "name": "python_test",
2612
+ "version": "0.0.1",
2613
+ }],
2614
+ # claim version 2 is the default
2615
+ # "claim_version": 2,
2616
+ "format": "image/jpeg",
2617
+ "title": "Python Test Image V2",
2618
+ "ingredients": [],
2619
+ "assertions": [
2620
+ {
2621
+ "label": "c2pa.actions",
2622
+ "data": {
2623
+ "actions": [
2624
+ {
2625
+ "action": "c2pa.created",
2626
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
2627
+ }
2628
+ ]
2629
+ }
2630
+ }
2631
+ ]
2632
+ }
2633
+ builder = Builder.from_json(initial_manifest_definition)
2634
+
2635
+ action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}'
2636
+ builder.add_action(action_json)
2637
+
2638
+ with open(self.testPath2, "rb") as file:
2639
+ output = io.BytesIO(bytearray())
2640
+ builder.sign(self.signer, "image/jpeg", file, output)
2641
+ output.seek(0)
2642
+ reader = Reader("image/jpeg", output)
2643
+ json_data = reader.json()
2644
+ manifest_data = json.loads(json_data)
2645
+
2646
+ # Verify active manifest exists
2647
+ self.assertIn("active_manifest", manifest_data)
2648
+ active_manifest_id = manifest_data["active_manifest"]
2649
+
2650
+ # Verify active manifest object exists
2651
+ self.assertIn("manifests", manifest_data)
2652
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2653
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2654
+
2655
+ # Verify assertions object exists in active manifest
2656
+ self.assertIn("assertions", active_manifest)
2657
+ assertions = active_manifest["assertions"]
2658
+
2659
+ # Find the c2pa.actions.v2 assertion to check what we added
2660
+ actions_assertion = None
2661
+ for assertion in assertions:
2662
+ if assertion.get("label") == "c2pa.actions.v2":
2663
+ actions_assertion = assertion
2664
+ break
2665
+
2666
+ self.assertIsNotNone(actions_assertion)
2667
+ self.assertIn("data", actions_assertion)
2668
+ assertion_data = actions_assertion["data"]
2669
+ # Verify the manifest now contains actions
2670
+ self.assertIn("actions", assertion_data)
2671
+ actions = assertion_data["actions"]
2672
+ # Verify "c2pa.color_adjustments" action exists anywhere in the actions array
2673
+ created_action_found = False
2674
+ for action in actions:
2675
+ if action.get("action") == "c2pa.color_adjustments":
2676
+ created_action_found = True
2677
+ break
2678
+
2679
+ self.assertTrue(created_action_found)
2680
+
2681
+ # Verify "c2pa.created" action exists only once in the actions array
2682
+ created_count = 0
2683
+ for action in actions:
2684
+ if action.get("action") == "c2pa.created":
2685
+ created_count += 1
2686
+
2687
+ self.assertEqual(created_count, 1, "c2pa.created action should appear exactly once")
2688
+
2689
+ builder.close()
2690
+
2691
+ # Reset settings to default
2692
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2693
+
2694
+ def test_builder_minimal_manifest_add_actions_and_sign_no_auto_add(self):
2695
+ # For testing, remove auto-added actions
2696
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
2697
+
2698
+ initial_manifest_definition = {
2699
+ "claim_generator": "python_test",
2700
+ "claim_generator_info": [{
2701
+ "name": "python_test",
2702
+ "version": "0.0.1",
2703
+ }],
2704
+ "format": "image/jpeg",
2705
+ "title": "Python Test Image V2",
2706
+ }
2707
+
2708
+ builder = Builder.from_json(initial_manifest_definition)
2709
+ builder.add_action('{ "action": "c2pa.created", "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"}')
2710
+
2711
+ with open(self.testPath2, "rb") as file:
2712
+ output = io.BytesIO(bytearray())
2713
+ builder.sign(self.signer, "image/jpeg", file, output)
2714
+ output.seek(0)
2715
+ reader = Reader("image/jpeg", output)
2716
+ json_data = reader.json()
2717
+ manifest_data = json.loads(json_data)
2718
+
2719
+ # Verify active manifest exists
2720
+ self.assertIn("active_manifest", manifest_data)
2721
+ active_manifest_id = manifest_data["active_manifest"]
2722
+
2723
+ # Verify active manifest object exists
2724
+ self.assertIn("manifests", manifest_data)
2725
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2726
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2727
+
2728
+ # Verify assertions object exists in active manifest
2729
+ self.assertIn("assertions", active_manifest)
2730
+ assertions = active_manifest["assertions"]
2731
+
2732
+ # Find the c2pa.actions.v2 assertion to look for what we added
2733
+ actions_assertion = None
2734
+ for assertion in assertions:
2735
+ if assertion.get("label") == "c2pa.actions.v2":
2736
+ actions_assertion = assertion
2737
+ break
2738
+
2739
+ self.assertIsNotNone(actions_assertion)
2740
+ self.assertIn("data", actions_assertion)
2741
+ assertion_data = actions_assertion["data"]
2742
+ # Verify the manifest now contains actions
2743
+ self.assertIn("actions", assertion_data)
2744
+ actions = assertion_data["actions"]
2745
+ # Verify "c2pa.created" action exists anywhere in the actions array
2746
+ created_action_found = False
2747
+ for action in actions:
2748
+ if action.get("action") == "c2pa.created":
2749
+ created_action_found = True
2750
+ break
2751
+
2752
+ self.assertTrue(created_action_found)
2753
+
2754
+ builder.close()
2755
+
2756
+ # Reset settings
2757
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2758
+
2759
+ def test_builder_minimal_manifest_add_actions_and_sign_with_auto_add(self):
2760
+ # For testing, remove auto-added actions
2761
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2762
+
2763
+ initial_manifest_definition = {
2764
+ "claim_generator_info": [{
2765
+ "name": "python_test",
2766
+ "version": "0.0.1",
2767
+ }],
2768
+ "format": "image/jpeg",
2769
+ "title": "Python Test Image V2",
2770
+ }
2771
+
2772
+ builder = Builder.from_json(initial_manifest_definition)
2773
+ action_json = '{"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}'
2774
+ builder.add_action(action_json)
2775
+
2776
+ with open(self.testPath2, "rb") as file:
2777
+ output = io.BytesIO(bytearray())
2778
+ builder.sign(self.signer, "image/jpeg", file, output)
2779
+ output.seek(0)
2780
+ reader = Reader("image/jpeg", output)
2781
+ json_data = reader.json()
2782
+ manifest_data = json.loads(json_data)
2783
+
2784
+ # Verify active manifest exists
2785
+ self.assertIn("active_manifest", manifest_data)
2786
+ active_manifest_id = manifest_data["active_manifest"]
2787
+
2788
+ # Verify active manifest object exists
2789
+ self.assertIn("manifests", manifest_data)
2790
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2791
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2792
+
2793
+ # Verify assertions object exists in active manifest
2794
+ self.assertIn("assertions", active_manifest)
2795
+ assertions = active_manifest["assertions"]
2796
+
2797
+ # Find the c2pa.actions.v2 assertion to look for what we added
2798
+ actions_assertion = None
2799
+ for assertion in assertions:
2800
+ if assertion.get("label") == "c2pa.actions.v2":
2801
+ actions_assertion = assertion
2802
+ break
2803
+
2804
+ self.assertIsNotNone(actions_assertion)
2805
+ self.assertIn("data", actions_assertion)
2806
+ assertion_data = actions_assertion["data"]
2807
+ # Verify the manifest now contains actions
2808
+ self.assertIn("actions", assertion_data)
2809
+ actions = assertion_data["actions"]
2810
+ # Verify "c2pa.created" action exists anywhere in the actions array
2811
+ created_action_found = False
2812
+ for action in actions:
2813
+ if action.get("action") == "c2pa.created":
2814
+ created_action_found = True
2815
+ break
2816
+
2817
+ self.assertTrue(created_action_found)
2818
+
2819
+ # Verify "c2pa.color_adjustments" action also exists in the same actions array
2820
+ color_adjustments_found = False
2821
+ for action in actions:
2822
+ if action.get("action") == "c2pa.color_adjustments":
2823
+ color_adjustments_found = True
2824
+ break
2825
+
2826
+ self.assertTrue(color_adjustments_found)
2827
+
2828
+ builder.close()
2829
+
2830
+ # Reset settings
2831
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2832
+
2833
+ def test_builder_sign_dicts_no_auto_add(self):
2834
+ # For testing, remove auto-added actions
2835
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":false},"auto_opened_action":{"enabled":false},"auto_created_action":{"enabled":false}}}}')
2836
+
2837
+ initial_manifest_definition = {
2838
+ "claim_generator_info": [{
2839
+ "name": "python_test",
2840
+ "version": "0.0.1",
2841
+ }],
2842
+ # claim version 2 is the default
2843
+ # "claim_version": 2,
2844
+ "format": "image/jpeg",
2845
+ "title": "Python Test Image V2",
2846
+ "assertions": [
2847
+ {
2848
+ "label": "c2pa.actions",
2849
+ "data": {
2850
+ "actions": [
2851
+ {
2852
+ "action": "c2pa.created",
2853
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
2854
+ }
2855
+ ]
2856
+ }
2857
+ }
2858
+ ]
2859
+ }
2860
+ builder = Builder.from_json(initial_manifest_definition)
2861
+
2862
+ # Using a dictionary instead of a JSON string
2863
+ action_dict = {"action": "c2pa.color_adjustments", "parameters": {"name": "brightnesscontrast"}}
2864
+ builder.add_action(action_dict)
2865
+
2866
+ with open(self.testPath2, "rb") as file:
2867
+ output = io.BytesIO(bytearray())
2868
+ builder.sign(self.signer, "image/jpeg", file, output)
2869
+ output.seek(0)
2870
+ reader = Reader("image/jpeg", output)
2871
+ json_data = reader.json()
2872
+ manifest_data = json.loads(json_data)
2873
+
2874
+ # Verify active manifest exists
2875
+ self.assertIn("active_manifest", manifest_data)
2876
+ active_manifest_id = manifest_data["active_manifest"]
2877
+
2878
+ # Verify active manifest object exists
2879
+ self.assertIn("manifests", manifest_data)
2880
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
2881
+ active_manifest = manifest_data["manifests"][active_manifest_id]
2882
+
2883
+ # Verify assertions object exists in active manifest
2884
+ self.assertIn("assertions", active_manifest)
2885
+ assertions = active_manifest["assertions"]
2886
+
2887
+ # Find the c2pa.actions.v2 assertion to check what we added
2888
+ actions_assertion = None
2889
+ for assertion in assertions:
2890
+ if assertion.get("label") == "c2pa.actions.v2":
2891
+ actions_assertion = assertion
2892
+ break
2893
+
2894
+ self.assertIsNotNone(actions_assertion)
2895
+ self.assertIn("data", actions_assertion)
2896
+ assertion_data = actions_assertion["data"]
2897
+ # Verify the manifest now contains actions
2898
+ self.assertIn("actions", assertion_data)
2899
+ actions = assertion_data["actions"]
2900
+ # Verify "c2pa.color_adjustments" action exists anywhere in the actions array
2901
+ created_action_found = False
2902
+ for action in actions:
2903
+ if action.get("action") == "c2pa.color_adjustments":
2904
+ created_action_found = True
2905
+ break
2906
+
2907
+ self.assertTrue(created_action_found)
2908
+
2909
+ builder.close()
2910
+
2911
+ # Reset settings
2912
+ load_settings('{"builder":{"actions":{"auto_placed_action":{"enabled":true},"auto_opened_action":{"enabled":true},"auto_created_action":{"enabled":true}}}}')
2913
+
2326
2914
 
2327
2915
  class TestStream(unittest.TestCase):
2328
2916
  def setUp(self):
@@ -2786,6 +3374,46 @@ class TestLegacyAPI(unittest.TestCase):
2786
3374
 
2787
3375
  builder.close()
2788
3376
 
3377
+ def test_builder_sign_with_ingredient_dict_from_file(self):
3378
+ """Test Builder class operations with an ingredient added from file path using a dictionary."""
3379
+
3380
+ builder = Builder.from_json(self.manifestDefinition)
3381
+
3382
+ # Test adding ingredient from file path with a dictionary
3383
+ ingredient_dict = {"title": "Test Ingredient From File"}
3384
+ # Suppress the specific deprecation warning for this test, as this is a legacy method
3385
+ with warnings.catch_warnings():
3386
+ warnings.simplefilter("ignore", DeprecationWarning)
3387
+ builder.add_ingredient_from_file_path(ingredient_dict, "image/jpeg", self.testPath3)
3388
+
3389
+ with open(self.testPath2, "rb") as file:
3390
+ output = io.BytesIO(bytearray())
3391
+ builder.sign(self.signer, "image/jpeg", file, output)
3392
+ output.seek(0)
3393
+ reader = Reader("image/jpeg", output)
3394
+ json_data = reader.json()
3395
+ manifest_data = json.loads(json_data)
3396
+
3397
+ # Verify active manifest exists
3398
+ self.assertIn("active_manifest", manifest_data)
3399
+ active_manifest_id = manifest_data["active_manifest"]
3400
+
3401
+ # Verify active manifest object exists
3402
+ self.assertIn("manifests", manifest_data)
3403
+ self.assertIn(active_manifest_id, manifest_data["manifests"])
3404
+ active_manifest = manifest_data["manifests"][active_manifest_id]
3405
+
3406
+ # Verify ingredients array exists in active manifest
3407
+ self.assertIn("ingredients", active_manifest)
3408
+ self.assertIsInstance(active_manifest["ingredients"], list)
3409
+ self.assertTrue(len(active_manifest["ingredients"]) > 0)
3410
+
3411
+ # Verify the first ingredient's title matches what we set
3412
+ first_ingredient = active_manifest["ingredients"][0]
3413
+ self.assertEqual(first_ingredient["title"], "Test Ingredient From File")
3414
+
3415
+ builder.close()
3416
+
2789
3417
  def test_builder_add_ingredient_from_file_path(self):
2790
3418
  """Test Builder class add_ingredient_from_file_path method."""
2791
3419
 
File without changes
File without changes
File without changes
File without changes
File without changes