Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 31 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -22,17 +22,17 @@ 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
```

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:

```
```bash
pip install --upgrade --force-reinstall c2pa-python
```

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -295,37 +295,38 @@ 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`:

```
```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
python3 -m venv .venv
```

Build the wheel for your platform:
Build the wheel for your platform (from the root of the repository):

```
```bash
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

Expand All @@ -336,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"
Expand All @@ -347,28 +348,27 @@ 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 following 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:

```
```bash
source .venv/bin/activate
maturin develop
python3 tests/training.py
Expand Down Expand Up @@ -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`.
Expand All @@ -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).


60 changes: 29 additions & 31 deletions c2pa/c2pa_api/c2pa_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
import os
import sys
import tempfile
import shutil

PROJECT_PATH = os.getcwd()
SOURCE_PATH = os.path.join(
PROJECT_PATH,"target","python"
)
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.
Expand All @@ -45,29 +44,29 @@ 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())
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))

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.
Expand Down Expand Up @@ -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)
with open(path, "rb") as file:
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)
with open(path, "rb") as file:
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)

Expand Down Expand Up @@ -172,14 +171,14 @@ 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
#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
Expand All @@ -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)



Expand All @@ -210,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,
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
maturin==1.2.0
uniffi-bindgen==0.24.1
uniffi-bindgen==0.24.1
cryptography==43.0.1
2 changes: 1 addition & 1 deletion src/c2pa.udl
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface CallbackSigner {

interface Builder {
constructor();

[Throws=Error]
void with_json([ByRef] string json);

Expand Down
10 changes: 5 additions & 5 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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():
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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)
#exit(1)
Loading
Loading