c2pa-python 0.25.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.
- {c2pa_python-0.25.0/src/c2pa_python.egg-info → c2pa_python-0.27.0}/PKG-INFO +1 -1
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/pyproject.toml +1 -1
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/c2pa.py +243 -11
- {c2pa_python-0.25.0 → c2pa_python-0.27.0/src/c2pa_python.egg-info}/PKG-INFO +1 -1
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/tests/test_unit_tests.py +865 -1
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/tests/test_unit_tests_threaded.py +150 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/LICENSE-APACHE +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/LICENSE-MIT +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/MANIFEST.in +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/README.md +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/requirements.txt +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/scripts/download_artifacts.py +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/setup.cfg +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/setup.py +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/__init__.py +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/build.py +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa/lib.py +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/SOURCES.txt +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/dependency_links.txt +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/entry_points.txt +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/requires.txt +0 -0
- {c2pa_python-0.25.0 → c2pa_python-0.27.0}/src/c2pa_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 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.
|
|
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
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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:
|
|
@@ -1358,6 +1386,10 @@ class Reader:
|
|
|
1358
1386
|
# we may have opened ourselves, and that we need to close later
|
|
1359
1387
|
self._backing_file = None
|
|
1360
1388
|
|
|
1389
|
+
# Caches for manifest JSON string and parsed data
|
|
1390
|
+
self._manifest_json_str_cache = None
|
|
1391
|
+
self._manifest_data_cache = None
|
|
1392
|
+
|
|
1361
1393
|
if stream is None:
|
|
1362
1394
|
# If we don't get a stream as param:
|
|
1363
1395
|
# Create a stream from the file path in format_or_path
|
|
@@ -1600,6 +1632,33 @@ class Reader:
|
|
|
1600
1632
|
# Ensure we don't raise exceptions during cleanup
|
|
1601
1633
|
pass
|
|
1602
1634
|
|
|
1635
|
+
def _get_cached_manifest_data(self) -> Optional[dict]:
|
|
1636
|
+
"""Get the cached manifest data, fetching and parsing if not cached.
|
|
1637
|
+
|
|
1638
|
+
Returns:
|
|
1639
|
+
A dictionary containing the parsed manifest data, or None if
|
|
1640
|
+
JSON parsing fails
|
|
1641
|
+
|
|
1642
|
+
Raises:
|
|
1643
|
+
C2paError: If there was an error getting the JSON
|
|
1644
|
+
"""
|
|
1645
|
+
if self._manifest_data_cache is None:
|
|
1646
|
+
if self._manifest_json_str_cache is None:
|
|
1647
|
+
self._manifest_json_str_cache = self.json()
|
|
1648
|
+
|
|
1649
|
+
try:
|
|
1650
|
+
self._manifest_data_cache = json.loads(
|
|
1651
|
+
self._manifest_json_str_cache
|
|
1652
|
+
)
|
|
1653
|
+
except json.JSONDecodeError:
|
|
1654
|
+
# Reset cache to reattempt read, possibly
|
|
1655
|
+
self._manifest_data_cache = None
|
|
1656
|
+
self._manifest_json_str_cache = None
|
|
1657
|
+
# Failed to parse manifest JSON
|
|
1658
|
+
return None
|
|
1659
|
+
|
|
1660
|
+
return self._manifest_data_cache
|
|
1661
|
+
|
|
1603
1662
|
def close(self):
|
|
1604
1663
|
"""Release the reader resources.
|
|
1605
1664
|
|
|
@@ -1620,6 +1679,9 @@ class Reader:
|
|
|
1620
1679
|
Reader._ERROR_MESSAGES['cleanup_error'].format(
|
|
1621
1680
|
str(e)))
|
|
1622
1681
|
finally:
|
|
1682
|
+
# Clear the cache when closing
|
|
1683
|
+
self._manifest_json_str_cache = None
|
|
1684
|
+
self._manifest_data_cache = None
|
|
1623
1685
|
self._closed = True
|
|
1624
1686
|
|
|
1625
1687
|
def json(self) -> str:
|
|
@@ -1634,6 +1696,10 @@ class Reader:
|
|
|
1634
1696
|
|
|
1635
1697
|
self._ensure_valid_state()
|
|
1636
1698
|
|
|
1699
|
+
# Return cached result if available
|
|
1700
|
+
if self._manifest_json_str_cache is not None:
|
|
1701
|
+
return self._manifest_json_str_cache
|
|
1702
|
+
|
|
1637
1703
|
result = _lib.c2pa_reader_json(self._reader)
|
|
1638
1704
|
|
|
1639
1705
|
if result is None:
|
|
@@ -1642,7 +1708,128 @@ class Reader:
|
|
|
1642
1708
|
raise C2paError(error)
|
|
1643
1709
|
raise C2paError("Error during manifest parsing in Reader")
|
|
1644
1710
|
|
|
1645
|
-
return
|
|
1711
|
+
# Cache the result and return it
|
|
1712
|
+
self._manifest_json_str_cache = _convert_to_py_string(result)
|
|
1713
|
+
return self._manifest_json_str_cache
|
|
1714
|
+
|
|
1715
|
+
def get_active_manifest(self) -> Optional[dict]:
|
|
1716
|
+
"""Get the active manifest from the manifest store.
|
|
1717
|
+
|
|
1718
|
+
This method retrieves the full manifest JSON and extracts the active
|
|
1719
|
+
manifest based on the active_manifest key.
|
|
1720
|
+
|
|
1721
|
+
Returns:
|
|
1722
|
+
A dictionary containing the active manifest data, including claims,
|
|
1723
|
+
assertions, ingredients, and signature information, or None if no
|
|
1724
|
+
manifest is found or if there was an error parsing the JSON.
|
|
1725
|
+
|
|
1726
|
+
Raises:
|
|
1727
|
+
KeyError: If the active_manifest key is missing from the JSON
|
|
1728
|
+
"""
|
|
1729
|
+
try:
|
|
1730
|
+
# Get cached manifest data
|
|
1731
|
+
manifest_data = self._get_cached_manifest_data()
|
|
1732
|
+
if manifest_data is None:
|
|
1733
|
+
# raise C2paError("Failed to parse manifest JSON")
|
|
1734
|
+
return None
|
|
1735
|
+
|
|
1736
|
+
# Get the active manfiest id/label
|
|
1737
|
+
if "active_manifest" not in manifest_data:
|
|
1738
|
+
raise KeyError("No 'active_manifest' key found")
|
|
1739
|
+
|
|
1740
|
+
active_manifest_id = manifest_data["active_manifest"]
|
|
1741
|
+
|
|
1742
|
+
# Retrieve the active manifest data using manifest id/label
|
|
1743
|
+
if "manifests" not in manifest_data:
|
|
1744
|
+
raise KeyError("No 'manifests' key found in manifest data")
|
|
1745
|
+
|
|
1746
|
+
manifests = manifest_data["manifests"]
|
|
1747
|
+
if active_manifest_id not in manifests:
|
|
1748
|
+
raise KeyError("Active manifest not found in manifest store")
|
|
1749
|
+
|
|
1750
|
+
return manifests[active_manifest_id]
|
|
1751
|
+
except C2paError.ManifestNotFound:
|
|
1752
|
+
return None
|
|
1753
|
+
|
|
1754
|
+
def get_manifest(self, label: str) -> Optional[dict]:
|
|
1755
|
+
"""Get a specific manifest from the manifest store by its label.
|
|
1756
|
+
|
|
1757
|
+
This method retrieves the manifest JSON and extracts the manifest
|
|
1758
|
+
that corresponds to the provided manifest label/ID.
|
|
1759
|
+
|
|
1760
|
+
Args:
|
|
1761
|
+
label: The manifest label/ID to look up in the manifest store
|
|
1762
|
+
|
|
1763
|
+
Returns:
|
|
1764
|
+
A dictionary containing the manifest data for the specified label,
|
|
1765
|
+
or None if no manifest is found or if there was an error parsing
|
|
1766
|
+
the JSON.
|
|
1767
|
+
|
|
1768
|
+
Raises:
|
|
1769
|
+
KeyError: If the manifests key is missing from the JSON
|
|
1770
|
+
"""
|
|
1771
|
+
try:
|
|
1772
|
+
# Get cached manifest data
|
|
1773
|
+
manifest_data = self._get_cached_manifest_data()
|
|
1774
|
+
if manifest_data is None:
|
|
1775
|
+
# raise C2paError("Failed to parse manifest JSON")
|
|
1776
|
+
return None
|
|
1777
|
+
|
|
1778
|
+
if "manifests" not in manifest_data:
|
|
1779
|
+
raise KeyError("No 'manifests' key found in manifest data")
|
|
1780
|
+
|
|
1781
|
+
manifests = manifest_data["manifests"]
|
|
1782
|
+
if label not in manifests:
|
|
1783
|
+
raise KeyError(f"Manifest {label} not found in manifest store")
|
|
1784
|
+
|
|
1785
|
+
return manifests[label]
|
|
1786
|
+
except C2paError.ManifestNotFound:
|
|
1787
|
+
return None
|
|
1788
|
+
|
|
1789
|
+
def get_validation_state(self) -> Optional[str]:
|
|
1790
|
+
"""Get the validation state of the manifest store.
|
|
1791
|
+
|
|
1792
|
+
This method retrieves the full manifest JSON and extracts the
|
|
1793
|
+
validation_state field, which indicates the overall validation
|
|
1794
|
+
status of the C2PA manifest.
|
|
1795
|
+
|
|
1796
|
+
Returns:
|
|
1797
|
+
The validation state as a string,
|
|
1798
|
+
or None if the validation_state field is not present or if no
|
|
1799
|
+
manifest is found or if there was an error parsing the JSON.
|
|
1800
|
+
"""
|
|
1801
|
+
try:
|
|
1802
|
+
# Get cached manifest data
|
|
1803
|
+
manifest_data = self._get_cached_manifest_data()
|
|
1804
|
+
if manifest_data is None:
|
|
1805
|
+
return None
|
|
1806
|
+
|
|
1807
|
+
return manifest_data.get("validation_state")
|
|
1808
|
+
except C2paError.ManifestNotFound:
|
|
1809
|
+
return None
|
|
1810
|
+
|
|
1811
|
+
def get_validation_results(self) -> Optional[dict]:
|
|
1812
|
+
"""Get the validation results of the manifest store.
|
|
1813
|
+
|
|
1814
|
+
This method retrieves the full manifest JSON and extracts
|
|
1815
|
+
the validation_results object, which contains detailed
|
|
1816
|
+
validation information.
|
|
1817
|
+
|
|
1818
|
+
Returns:
|
|
1819
|
+
The validation results as a dictionary containing
|
|
1820
|
+
validation details, or None if the validation_results
|
|
1821
|
+
field is not present or if no manifest is found or if
|
|
1822
|
+
there was an error parsing the JSON.
|
|
1823
|
+
"""
|
|
1824
|
+
try:
|
|
1825
|
+
# Get cached manifest data
|
|
1826
|
+
manifest_data = self._get_cached_manifest_data()
|
|
1827
|
+
if manifest_data is None:
|
|
1828
|
+
return None
|
|
1829
|
+
|
|
1830
|
+
return manifest_data.get("validation_results")
|
|
1831
|
+
except C2paError.ManifestNotFound:
|
|
1832
|
+
return None
|
|
1646
1833
|
|
|
1647
1834
|
def resource_to_stream(self, uri: str, stream: Any) -> int:
|
|
1648
1835
|
"""Write a resource to a stream.
|
|
@@ -2039,6 +2226,7 @@ class Builder:
|
|
|
2039
2226
|
'url_error': "Error setting remote URL: {}",
|
|
2040
2227
|
'resource_error': "Error adding resource: {}",
|
|
2041
2228
|
'ingredient_error': "Error adding ingredient: {}",
|
|
2229
|
+
'action_error': "Error adding action: {}",
|
|
2042
2230
|
'archive_error': "Error writing archive: {}",
|
|
2043
2231
|
'sign_error': "Error during signing: {}",
|
|
2044
2232
|
'encoding_error': "Invalid UTF-8 characters in manifest: {}",
|
|
@@ -2336,13 +2524,16 @@ class Builder:
|
|
|
2336
2524
|
)
|
|
2337
2525
|
)
|
|
2338
2526
|
|
|
2339
|
-
def add_ingredient(
|
|
2527
|
+
def add_ingredient(
|
|
2528
|
+
self, ingredient_json: Union[str, dict], format: str, source: Any
|
|
2529
|
+
):
|
|
2340
2530
|
"""Add an ingredient to the builder (facade method).
|
|
2341
2531
|
The added ingredient's source should be a stream-like object
|
|
2342
2532
|
(for instance, a file opened as stream).
|
|
2343
2533
|
|
|
2344
2534
|
Args:
|
|
2345
2535
|
ingredient_json: The JSON ingredient definition
|
|
2536
|
+
(either a JSON string or a dictionary)
|
|
2346
2537
|
format: The MIME type or extension of the ingredient
|
|
2347
2538
|
source: The stream containing the ingredient data
|
|
2348
2539
|
(any Python stream-like object)
|
|
@@ -2362,7 +2553,7 @@ class Builder:
|
|
|
2362
2553
|
|
|
2363
2554
|
def add_ingredient_from_stream(
|
|
2364
2555
|
self,
|
|
2365
|
-
ingredient_json: str,
|
|
2556
|
+
ingredient_json: Union[str, dict],
|
|
2366
2557
|
format: str,
|
|
2367
2558
|
source: Any):
|
|
2368
2559
|
"""Add an ingredient from a stream to the builder.
|
|
@@ -2370,6 +2561,7 @@ class Builder:
|
|
|
2370
2561
|
|
|
2371
2562
|
Args:
|
|
2372
2563
|
ingredient_json: The JSON ingredient definition
|
|
2564
|
+
(either a JSON string or a dictionary)
|
|
2373
2565
|
format: The MIME type or extension of the ingredient
|
|
2374
2566
|
source: The stream containing the ingredient data
|
|
2375
2567
|
(any Python stream-like object)
|
|
@@ -2381,6 +2573,9 @@ class Builder:
|
|
|
2381
2573
|
"""
|
|
2382
2574
|
self._ensure_valid_state()
|
|
2383
2575
|
|
|
2576
|
+
if isinstance(ingredient_json, dict):
|
|
2577
|
+
ingredient_json = json.dumps(ingredient_json)
|
|
2578
|
+
|
|
2384
2579
|
try:
|
|
2385
2580
|
ingredient_str = ingredient_json.encode('utf-8')
|
|
2386
2581
|
format_str = format.encode('utf-8')
|
|
@@ -2411,7 +2606,7 @@ class Builder:
|
|
|
2411
2606
|
|
|
2412
2607
|
def add_ingredient_from_file_path(
|
|
2413
2608
|
self,
|
|
2414
|
-
ingredient_json: str,
|
|
2609
|
+
ingredient_json: Union[str, dict],
|
|
2415
2610
|
format: str,
|
|
2416
2611
|
filepath: Union[str, Path]):
|
|
2417
2612
|
"""Add an ingredient from a file path to the builder (deprecated).
|
|
@@ -2423,6 +2618,7 @@ class Builder:
|
|
|
2423
2618
|
|
|
2424
2619
|
Args:
|
|
2425
2620
|
ingredient_json: The JSON ingredient definition
|
|
2621
|
+
(either a JSON string or a dictionary)
|
|
2426
2622
|
format: The MIME type or extension of the ingredient
|
|
2427
2623
|
filepath: The path to the file containing the ingredient data
|
|
2428
2624
|
(can be a string or Path object)
|
|
@@ -2454,6 +2650,42 @@ class Builder:
|
|
|
2454
2650
|
except Exception as e:
|
|
2455
2651
|
raise C2paError.Other(f"Could not add ingredient: {e}") from e
|
|
2456
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
|
+
|
|
2457
2689
|
def to_archive(self, stream: Any) -> None:
|
|
2458
2690
|
"""Write an archive of the builder to a stream.
|
|
2459
2691
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: c2pa-python
|
|
3
|
-
Version: 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>
|