From 8641a2342246b22f2d5815e1e6c19cb8379393b0 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 8 Oct 2024 14:06:28 -0700 Subject: [PATCH 1/9] with file --- c2pa/c2pa_api/c2pa_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/c2pa/c2pa_api/c2pa_api.py b/c2pa/c2pa_api/c2pa_api.py index 26fecd86..1095266b 100644 --- a/c2pa/c2pa_api/c2pa_api.py +++ b/c2pa/c2pa_api/c2pa_api.py @@ -45,11 +45,11 @@ def __init__(self, format, stream, manifest_data=None): @classmethod def from_file(cls, path: str, format=None): - file = open(path, "rb") - if format is None: - # determine the format from the file extension - format = os.path.splitext(path)[1][1:] - return cls(format, file) + with open(path, "rb") as file: + if format is None: + # determine the format from the file extension + format = os.path.splitext(path)[1][1:] + return cls(format, file) def get_manifest(self, label): manifest_store = json.loads(self.json()) From 3452770ad9b5aad2c85da66746b6fe0a13698320 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 8 Oct 2024 14:14:43 -0700 Subject: [PATCH 2/9] Remove unused dependency --- c2pa/c2pa_api/c2pa_api.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/c2pa/c2pa_api/c2pa_api.py b/c2pa/c2pa_api/c2pa_api.py index 1095266b..6ba472b2 100644 --- a/c2pa/c2pa_api/c2pa_api.py +++ b/c2pa/c2pa_api/c2pa_api.py @@ -15,7 +15,7 @@ import os import sys import tempfile -import shutil + PROJECT_PATH = os.getcwd() SOURCE_PATH = os.path.join( PROJECT_PATH,"target","python" @@ -23,7 +23,6 @@ sys.path.append(SOURCE_PATH) import c2pa.c2pa as api - #from c2pa import Error, SigningAlg, version, sdk_version # This module provides a simple Python API for the C2PA library. @@ -50,18 +49,18 @@ def from_file(cls, path: str, format=None): # determine the format from the file extension format = os.path.splitext(path)[1][1:] return cls(format, file) - + def get_manifest(self, label): manifest_store = json.loads(self.json()) return manifest_store["manifests"].get(label) - + def get_active_manifest(self): manifest_store = json.loads(self.json()) active_label = manifest_store.get("active_manifest") if active_label: return manifest_store["manifests"].get(active_label) return None - + def resource_to_stream(self, uri, stream) -> None: return super().resource_to_stream(uri, C2paStream(stream)) @@ -101,49 +100,49 @@ def set_manifest(self, manifest): manifest = json.dumps(manifest) super().with_json(manifest) return self - + def add_resource(self, uri, stream): return super().add_resource(uri, C2paStream(stream)) - + def add_resource_file(self, uri, path): file = open(path, "rb") return self.add_resource(uri, file) - + def add_ingredient(self, ingredient, format, stream): if not isinstance(ingredient, str): ingredient = json.dumps(ingredient) return super().add_ingredient(ingredient, format, C2paStream(stream)) - + def add_ingredient_file(self, ingredient, path): format = os.path.splitext(path)[1][1:] file = open(path, "rb") return self.add_ingredient(ingredient, format, file) - + def to_archive(self, stream): return super().to_archive(C2paStream(stream)) - + @classmethod def from_archive(cls, stream): self = cls({}) super().from_archive(self, C2paStream(stream)) return self - + def sign(self, signer, format, input, output = None): return super().sign(signer, format, C2paStream(input), C2paStream(output)) def sign_file(self, signer, sourcePath, outputPath): return super().sign_file(signer, sourcePath, outputPath) - + # Implements a C2paStream given a stream handle # This is used to pass a file handle to the c2pa library -# It is used by the Reader and Builder classes internally +# It is used by the Reader and Builder classes internally class C2paStream(api.Stream): def __init__(self, stream): self.stream = stream - - def read_stream(self, length: int) -> bytes: + + def read_stream(self, length: int) -> bytes: #print("Reading " + str(length) + " bytes") return self.stream.read(length) @@ -179,7 +178,7 @@ def __init__(self, callback): #class CallbackSigner(c2pa.CallbackSigner): # def __init__(self, callback, alg, certs, timestamp_url=None): # cb = SignerCallback(callback) -# super().__init__(cb, alg, certs, timestamp_url) +# super().__init__(cb, alg, certs, timestamp_url) # Creates a Signer given a callback and configuration values # It is used by the Builder class to sign the asset @@ -192,7 +191,7 @@ def __init__(self, callback): # signer = c2pa_api.create_signer(sign_ps256, "ps256", certs, "http://timestamp.digicert.com") # def create_signer(callback, alg, certs, timestamp_url=None): - return api.CallbackSigner(SignerCallback(callback), alg, certs, timestamp_url) + return api.CallbackSigner(SignerCallback(callback), alg, certs, timestamp_url) From 78dade652b622e05a23a049b28e189559304f6b2 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 8 Oct 2024 14:58:10 -0700 Subject: [PATCH 3/9] File handling --- c2pa/c2pa_api/c2pa_api.py | 13 ++++++------- tests/test_api.py | 10 +++++----- tests/training.py | 11 +++++------ tests/unit_tests.py | 8 +++----- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/c2pa/c2pa_api/c2pa_api.py b/c2pa/c2pa_api/c2pa_api.py index 6ba472b2..22bd5970 100644 --- a/c2pa/c2pa_api/c2pa_api.py +++ b/c2pa/c2pa_api/c2pa_api.py @@ -65,8 +65,8 @@ def resource_to_stream(self, uri, stream) -> None: return super().resource_to_stream(uri, C2paStream(stream)) def resource_to_file(self, uri, path) -> None: - file = open(path, "wb") - return self.resource_to_stream(uri, file) + with open(path, "wb") as file: + return self.resource_to_stream(uri, file) # The Builder is used to construct a new Manifest and add it to a stream or file. # The initial manifest is defined by a Manifest Definition dictionary. @@ -105,8 +105,8 @@ def add_resource(self, uri, stream): return super().add_resource(uri, C2paStream(stream)) def add_resource_file(self, uri, path): - file = open(path, "rb") - return self.add_resource(uri, file) + with open(path, "rb") as file: + return self.add_resource(uri, file) def add_ingredient(self, ingredient, format, stream): if not isinstance(ingredient, str): @@ -115,8 +115,8 @@ def add_ingredient(self, ingredient, format, stream): def add_ingredient_file(self, ingredient, path): format = os.path.splitext(path)[1][1:] - file = open(path, "rb") - return self.add_ingredient(ingredient, format, file) + with open(path, "rb") as file: + return self.add_ingredient(ingredient, format, file) def to_archive(self, stream): return super().to_archive(C2paStream(stream)) @@ -209,7 +209,6 @@ def sign_ps256_shell(data: bytes, key_path: str) -> bytes: from cryptography.hazmat.primitives.asymmetric import padding def sign_ps256(data: bytes, key: bytes) -> bytes: - private_key = serialization.load_pem_private_key( key, password=None, diff --git a/tests/test_api.py b/tests/test_api.py index f9f57785..930064e0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -63,7 +63,7 @@ def test_sdk_version(): assert "c2pa-rs/" in sdk_version() def test_v2_read(): - #example of reading a manifest store from a file + #example of reading a manifest store from a file try: reader = Reader.from_file("tests/fixtures/C.jpg") manifest = reader.get_active_manifest() @@ -92,7 +92,7 @@ def test_v2_read(): exit(1) def test_reader_from_file_no_store(): - with pytest.raises(Error.ManifestNotFound) as err: + with pytest.raises(Error.ManifestNotFound) as err: reader = Reader.from_file("tests/fixtures/A.jpg") def test_v2_sign(): @@ -102,7 +102,7 @@ def test_v2_sign(): key = open(data_dir + "ps256.pem", "rb").read() def sign(data: bytes) -> bytes: return sign_ps256(data, key) - + certs = open(data_dir + "ps256.pub", "rb").read() # Create a local signer from a certificate pem file signer = create_signer(sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com") @@ -143,7 +143,7 @@ def test_v2_sign_file_same(): key = open(data_dir + "ps256.pem", "rb").read() def sign(data: bytes) -> bytes: return sign_ps256(data, key) - + certs = open(data_dir + "ps256.pub", "rb").read() # Create a local signer from a certificate pem file signer = create_signer(sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com") @@ -169,4 +169,4 @@ def sign(data: bytes) -> bytes: assert manifest.get("validation_status") == None except Exception as e: print("Failed to sign manifest store: " + str(e)) - #exit(1) \ No newline at end of file + #exit(1) diff --git a/tests/training.py b/tests/training.py index c02709ec..392009c5 100644 --- a/tests/training.py +++ b/tests/training.py @@ -59,7 +59,7 @@ def getitem(d, key): } } ] - } +} ingredient_json = { "title": "A.jpg", @@ -72,11 +72,11 @@ def getitem(d, key): # V2 signing api try: - # This could be implemented on a server using an HSM + # This could be implemented on a server using an HSM key = open("tests/fixtures/ps256.pem","rb").read() def sign(data: bytes) -> bytes: return sign_ps256(data, key) - + certs = open("tests/fixtures/ps256.pub","rb").read() # Create a signer from a certificate pem file @@ -92,7 +92,7 @@ def sign(data: bytes) -> bytes: os.remove(testOutputFile) result = builder.sign_file(signer, testFile, testOutputFile) - + except Exception as err: sys.exit(err) @@ -112,7 +112,7 @@ def sign(data: bytes) -> bytes: if getitem(assertion, ("data","entries","c2pa.ai_training","use")) == "notAllowed": allowed = False - # get the ingredient thumbnail + # get the ingredient thumbnail uri = getitem(manifest,("ingredients", 0, "thumbnail", "identifier")) reader.resource_to_file(uri, "target/thumbnail_v2.jpg") except Exception as err: @@ -122,4 +122,3 @@ def sign(data: bytes) -> bytes: print("Training is allowed") else: print("Training is not allowed") - diff --git a/tests/unit_tests.py b/tests/unit_tests.py index e3c8c940..c198dce9 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -24,7 +24,6 @@ testPath = os.path.join(PROJECT_PATH, "tests", "fixtures", "C.jpg") class TestC2paSdk(unittest.TestCase): - def test_version(self): self.assertIn("0.5.2", sdk_version()) @@ -39,12 +38,11 @@ def test_stream_read(self): def test_stream_read_and_parse(self): with open(testPath, "rb") as file: - reader = Reader("image/jpeg",file) + reader = Reader("image/jpeg", file) manifest_store = json.loads(reader.json()) - title = manifest = manifest_store["manifests"][manifest_store["active_manifest"]]["title"] + title = manifest_store["manifests"][manifest_store["active_manifest"]]["title"] self.assertEqual(title, "C.jpg") - def test_json_decode_err(self): with self.assertRaises(Error.Io): manifest_store = Reader("image/jpeg","foo") @@ -52,7 +50,7 @@ def test_json_decode_err(self): def test_reader_bad_format(self): with self.assertRaises(Error.NotSupported): with open(testPath, "rb") as file: - reader = Reader("badFormat",file) + reader = Reader("badFormat", file) class TestBuilder(unittest.TestCase): From cddaaa33ce8aa7f1c9700da8c15fb2a67e1814d7 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 8 Oct 2024 15:00:19 -0700 Subject: [PATCH 4/9] Fix indent --- c2pa/c2pa_api/c2pa_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/c2pa/c2pa_api/c2pa_api.py b/c2pa/c2pa_api/c2pa_api.py index 22bd5970..60ba2d85 100644 --- a/c2pa/c2pa_api/c2pa_api.py +++ b/c2pa/c2pa_api/c2pa_api.py @@ -106,7 +106,7 @@ def add_resource(self, uri, stream): def add_resource_file(self, uri, path): with open(path, "rb") as file: - return self.add_resource(uri, file) + return self.add_resource(uri, file) def add_ingredient(self, ingredient, format, stream): if not isinstance(ingredient, str): @@ -116,7 +116,7 @@ def add_ingredient(self, ingredient, format, stream): def add_ingredient_file(self, ingredient, path): format = os.path.splitext(path)[1][1:] with open(path, "rb") as file: - return self.add_ingredient(ingredient, format, file) + return self.add_ingredient(ingredient, format, file) def to_archive(self, stream): return super().to_archive(C2paStream(stream)) @@ -171,7 +171,7 @@ def open_file(path: str, mode: str) -> api.Stream: class SignerCallback(api.SignerCallback): def __init__(self, callback): self.sign = callback - super().__init__() + super().__init__() # Convenience class so we can just pass in a callback function From 8a53470afe8c541899da3564b754a26b524defe3 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Tue, 8 Oct 2024 15:01:28 -0700 Subject: [PATCH 5/9] Fix indent 2 --- c2pa/c2pa_api/c2pa_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/c2pa/c2pa_api/c2pa_api.py b/c2pa/c2pa_api/c2pa_api.py index 60ba2d85..7b430dd7 100644 --- a/c2pa/c2pa_api/c2pa_api.py +++ b/c2pa/c2pa_api/c2pa_api.py @@ -45,10 +45,10 @@ def __init__(self, format, stream, manifest_data=None): @classmethod def from_file(cls, path: str, format=None): with open(path, "rb") as file: - if format is None: - # determine the format from the file extension - format = os.path.splitext(path)[1][1:] - return cls(format, file) + if format is None: + # determine the format from the file extension + format = os.path.splitext(path)[1][1:] + return cls(format, file) def get_manifest(self, label): manifest_store = json.loads(self.json()) From 9dcc551003227211dea43724e97ff779078b2cb4 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Wed, 9 Oct 2024 08:41:09 -0700 Subject: [PATCH 6/9] Remove whitespace --- src/c2pa.udl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c2pa.udl b/src/c2pa.udl index 61905b7c..671a3ae8 100644 --- a/src/c2pa.udl +++ b/src/c2pa.udl @@ -78,7 +78,7 @@ interface CallbackSigner { interface Builder { constructor(); - + [Throws=Error] void with_json([ByRef] string json); From dea7abcc241068e497a1bfffd25eb16064eaf1e8 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Wed, 9 Oct 2024 11:33:19 -0700 Subject: [PATCH 7/9] Fix build and test run issues --- README.md | 22 ++++++++++------------ requirements.txt | 3 ++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8a9cd883..e3736f54 100644 --- a/README.md +++ b/README.md @@ -320,12 +320,11 @@ apt install python3.11-venv python3 -m venv .venv ``` -Build the wheel for your platform: +Build the wheel for your platform (from the root of the repository): ``` source .venv/bin/activate -pip install maturin -pip install uniffi-bindgen +pip install -r requirements.txt python3 -m pip install build pip install -U pytest @@ -347,24 +346,23 @@ pip install build pip install -U pytest cd home -git clone https://github.com/contentauth/c2pa-python.git +git clone https://github.com/contentauth/c2pa-python.git cd c2pa-python python3 -m build --wheel -auditwheel repair target/wheels/c2pa_python-0.4.0-py3-none-linux_aarch64.whl +auditwheel repair target/wheels/c2pa_python-0.4.0-py3-none-linux_aarch64.whl ``` ### Testing We use [PyTest](https://docs.pytest.org/) for testing. -Run tests by entering this command: +Run tests by fllowing these steps: -``` -source .venv/bin/activate -maturin develop -pytest -deactivate -``` +1. Activate the virtual environment: `source .venv/bin/activate` +2. (optional) Install dependencies: `pip install -r requirements.txt` +3. Setup the virtual environment with local changes: `maturin develop` +4. Run the tests: `pytest` +5. Deactivate the virtual environment: `deactivate` For example: diff --git a/requirements.txt b/requirements.txt index e7df30ef..98fba72e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ maturin==1.2.0 -uniffi-bindgen==0.24.1 \ No newline at end of file +uniffi-bindgen==0.24.1 +cryptography==43.0.1 \ No newline at end of file From 0537f98629393e047958a30d34c46b15e4e34924 Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Wed, 9 Oct 2024 11:46:15 -0700 Subject: [PATCH 8/9] Build readme --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e3736f54..ccc9fef8 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ pip list | grep c2pa-python If the version shown is lower than the most recent version, then update by [reinstalling](#installation). -### Reinstalling +### Reinstalling -If you tried unsuccessfully to install this package before the [0.40 release](https://github.com/contentauth/c2pa-python/releases/tag/v0.4), then use this command to reinstall: +If you tried unsuccessfully to install this package before the [0.40 release](https://github.com/contentauth/c2pa-python/releases/tag/v0.4), then use this command to reinstall: ``` pip install --upgrade --force-reinstall c2pa-python @@ -142,9 +142,9 @@ try: def private_sign(data: bytes) -> bytes: return sign_ps256(data, "tests/fixtures/ps256.pem") - # read our public certs into memory + # read our public certs into memory certs = open(data_dir + "ps256.pub", "rb").read() - + # Create a signer from the private signer, certs and a time stamp service url signer = create_signer(private_sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com") @@ -225,9 +225,9 @@ try: def private_sign(data: bytes) -> bytes: return sign_ps256(data, "tests/fixtures/ps256.pem") - # read our public certs into memory + # read our public certs into memory certs = open(data_dir + "ps256.pub", "rb").read() - + # Create a signer from the private signer, certs and a time stamp service url signer = create_signer(private_sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com") @@ -295,7 +295,9 @@ except Exception as err: ## Development -It is best to [set up a virtual environment](https://virtualenv.pypa.io/en/latest/installation.html) for development and testing. To build from source on Linux, install `curl` and `rustup` then set up Python. +It is best to [set up a virtual environment](https://virtualenv.pypa.io/en/latest/installation.html) for development and testing. + +To build from source on Linux, install `curl` and `rustup` then set up Python. First update `apt` then (if needed) install `curl`: @@ -356,7 +358,7 @@ auditwheel repair target/wheels/c2pa_python-0.4.0-py3-none-linux_aarch64.whl We use [PyTest](https://docs.pytest.org/) for testing. -Run tests by fllowing these steps: +Run tests by following these steps: 1. Activate the virtual environment: `source .venv/bin/activate` 2. (optional) Install dependencies: `pip install -r requirements.txt` @@ -366,7 +368,7 @@ Run tests by fllowing these steps: For example: -``` +```bash source .venv/bin/activate maturin develop python3 tests/training.py From ba83d4e3913e75c81dc9d80b9e98c8789ae169ec Mon Sep 17 00:00:00 2001 From: Tania Mathern Date: Wed, 9 Oct 2024 12:01:03 -0700 Subject: [PATCH 9/9] Build readme --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ccc9fef8..0620bd41 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This library enables you to read and validate C2PA data in supported media files Install from PyPI by entering this command: -``` +```bash pip install -U c2pa-python ``` @@ -22,7 +22,7 @@ This is a platform wheel built with Rust that works on Windows, macOS, and most Determine what version you've got by entering this command: -``` +```bash pip list | grep c2pa-python ``` @@ -32,7 +32,7 @@ If the version shown is lower than the most recent version, then update by [rein If you tried unsuccessfully to install this package before the [0.40 release](https://github.com/contentauth/c2pa-python/releases/tag/v0.4), then use this command to reinstall: -``` +```bash pip install --upgrade --force-reinstall c2pa-python ``` @@ -98,11 +98,11 @@ def sign_ps256(data: bytes, key_path: str) -> bytes: ### File-based operation -**Read and validate C2PA data from an asset file** +**Read and validate C2PA data from an asset file** Use the `Reader` to read C2PA data from the specified asset file (see [supported file formats](#supported-file-formats)). -This examines the specified media file for C2PA data and generates a report of any data it finds. If there are validation errors, the report includes a `validation_status` field. +This examines the specified media file for C2PA data and generates a report of any data it finds. If there are validation errors, the report includes a `validation_status` field. An asset file may contain many manifests in a manifest store. The most recent manifest is identified by the value of the `active_manifest` field in the manifests map. The manifests may contain binary resources such as thumbnails which can be retrieved with `resource_to_stream` or `resource_to_file` using the associated `identifier` field values and a `uri`. @@ -301,21 +301,21 @@ To build from source on Linux, install `curl` and `rustup` then set up Python. First update `apt` then (if needed) install `curl`: -``` +```bash apt update apt install curl ``` Install Rust: -``` +```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` Install Python, `pip`, and `venv`: -``` +```bash apt install python3 apt install pip apt install python3.11-venv @@ -324,7 +324,7 @@ python3 -m venv .venv Build the wheel for your platform (from the root of the repository): -``` +```bash source .venv/bin/activate pip install -r requirements.txt python3 -m pip install build @@ -337,7 +337,7 @@ python3 -m build --wheel Build using [manylinux](https://github.com/pypa/manylinux) by using a Docker image as follows: -``` +```bash docker run -it quay.io/pypa/manylinux_2_28_aarch64 bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" @@ -413,6 +413,7 @@ This release: ### Version 0.3.0 This release includes some breaking changes to align with future APIs: + - `C2paSignerInfo` moves the `alg` to the first parameter from the 3rd. - `c2pa.verify_from_file_json` is now `c2pa.read_file`. - `c2pa.ingredient_from_file_json` is now `c2pa.read_ingredient_file`. @@ -430,5 +431,3 @@ Note that some components and dependent crates are licensed under different term ### Contributions and feedback We welcome contributions to this project. For information on contributing, providing feedback, and about ongoing work, see [Contributing](https://github.com/contentauth/c2pa-python/blob/main/CONTRIBUTING.md). - -