Skip to content

Commit 536d133

Browse files
committed
Hybrid package including c2pa_api
Updates README
1 parent 399c692 commit 536d133

File tree

8 files changed

+153
-61
lines changed

8 files changed

+153
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ __pycache__/
2121
.pytest_cache/
2222
dist
2323

24+
c2pa/c2pa/
25+
2426
# Mac OS X files
2527
.DS_Store

README.md

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Python bindings for the C2PA Content Authenticity Initiative (CAI) library.
44

55
This library enables you to read and validate C2PA data in supported media files and add signed manifests to supported media files.
66

7-
**WARNING**: This is an early prerelease version of this library. There may be bugs and unimplemented features, and the API is subject to change.
7+
**NOTE**: This is a completely different API from 0.4.0. Check [Release notes](#release-notes) for changes.
8+
9+
**WARNING**: This is an prerelease version of this library. There may be bugs and unimplemented features, and the API is subject to change.
810

911
## Installation
1012

@@ -28,63 +30,119 @@ pip install --upgrade --force-reinstall c2pa-python
2830

2931
### Import
3032

31-
Import the C2PA module as follows:
33+
Import the API as follows:
3234

3335
```py
34-
import c2pa
36+
from c2pa import *
3537
```
3638

3739
### Read and validate C2PA data in a file
3840

39-
Use the `read_file` function to read C2PA data from the specified file:
40-
41-
```py
42-
json_store = c2pa.read_file("path/to/media_file.jpg", "path/to/data_dir")
43-
```
44-
45-
This function examines the specified media file for C2PA data and generates a JSON report of any data it finds. If there are validation errors, the report includes a `validation_status` field. For a summary of supported media types, see [Supported file formats](#supported-file-formats).
41+
Use the `Reader` to read C2PA data from the specified file.
42+
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. For a summary of supported media types, see [Supported file formats](#supported-file-formats).
4643

4744
A media 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.
4845

49-
If the optional `data_dir` is provided, the function extracts any binary resources, such as thumbnails, icons, and C2PA data into that directory. These files are referenced by the identifier fields in the manifest store report.
46+
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`.
5047

5148
NOTE: For a comprehensive reference to the JSON manifest structure, see the [Manifest store reference](https://opensource.contentauthenticity.org/docs/manifest/manifest-ref).
52-
53-
### Add a signed manifest to a media file
54-
55-
Use the `sign_file` function to add a signed manifest to a media file.
56-
5749
```py
58-
result = c2pa.sign_file("path/to/source.jpg",
59-
"path/to/dest.jpg",
60-
manifest_json,
61-
sign_info,
62-
data_dir)
63-
```
50+
try:
51+
reader = c2pa.Reader("path/to/media_file.jpg")
6452

65-
The parameters (in order) are:
66-
- The source (original) media file.
67-
- The destination file that will contain a copy of the source file with the manifest data added.
68-
- `manifest_json`, a JSON-formatted string containing the manifest data you want to add; see [Creating a manifest JSON definition file](#creating-a-manifest-json-definition-file) below.
69-
- `sign_info`, a `SignerInfo` object instance; see [Generating SignerInfo](#generating-signerinfo) below.
70-
- `data_dir` optionally specifies a directory path from which to load resource files referenced in the manifest JSON identifier fields; for example, thumbnails, icons, and manifest data for ingredients.
53+
# Print the JSON for a manifest.
54+
print("manifest store:", reader.json())
7155

72-
### Create a SignerInfo instance
56+
# Get the active manifest.
57+
manifest = reader.get_active_manifest()
58+
if manifest != None:
7359

74-
A `SignerInfo` object contains information about a signature. To create an instance of `SignerInfo`, first set up the signer information from the public and private key `.pem` files as follows:
60+
# get the uri to the manifest's thumbnail and write it to a file
61+
uri = manifest["thumbnail"]["identifier"]
62+
reader.resource_to_file(uri, "thumbnail_v2.jpg")
7563

76-
```py
77-
certs = open("path/to/public_certs.pem","rb").read()
78-
prv_key = open("path/to/private_key.pem","rb").read()
64+
except Exception as err:
65+
print(err)
7966
```
8067

81-
Then create a new `SignerInfo` instance using the keys as follows, specifying the signing algorithm used and optionally a time stamp authority URL:
68+
### Add a signed manifest to a media file
8269

83-
```py
84-
sign_info = c2pa.SignerInfo("es256", certs, priv_key, "http://timestamp.digicert.com")
85-
```
70+
Use a `Builder` to add a manifest to an asset.
8671

87-
For the list of supported signing algorithms, see [Creating and using an X.509 certificate](https://opensource.contentauthenticity.org/docs/c2patool/x_509).
72+
```py
73+
try:
74+
# Define a function to sign the claim bytes
75+
# In this case we are using a pre-defined sign_ps256 method, passing in our private cert
76+
# Normally this cert would be kept safe in some other location
77+
def private_sign(data: bytes) -> bytes:
78+
return sign_ps256(data, "tests/fixtures/ps256.pem")
79+
80+
# read our public certs into memory
81+
certs = open(data_dir + "ps256.pub", "rb").read()
82+
83+
# Create a signer from the private signer, certs and a time stamp service url
84+
signer = create_signer(private_sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com")
85+
86+
# Define a manifest with thumbnail and an assertion.
87+
manifest_json = {
88+
"claim_generator_info": [{
89+
"name": "python_test",
90+
"version": "0.1"
91+
}],
92+
"title": "Do Not Train Example",
93+
"thumbnail": {
94+
"format": "image/jpeg",
95+
"identifier": "thumbnail"
96+
},
97+
"assertions": [
98+
{
99+
"label": "c2pa.training-mining",
100+
"data": {
101+
"entries": {
102+
"c2pa.ai_generative_training": { "use": "notAllowed" },
103+
"c2pa.ai_inference": { "use": "notAllowed" },
104+
"c2pa.ai_training": { "use": "notAllowed" },
105+
"c2pa.data_mining": { "use": "notAllowed" }
106+
}
107+
}
108+
}
109+
]
110+
}
111+
112+
# Create a builder add a thumbnail resource and an ingredient file.
113+
builder = Builder(manifest_json)
114+
115+
# The uri provided here "thumbnail" must match an identifier in the manifest definition.
116+
builder.add_resource_file("thumbnail", "tests/fixtures/A_thumbnail.jpg")
117+
118+
# Define an ingredient, in this case a parent ingredient named A.jpg, with a thumbnail
119+
ingredient_json = {
120+
"title": "A.jpg",
121+
"relationship": "parentOf", # "parentOf", "componentOf" or "inputTo"
122+
"thumbnail": {
123+
"identifier": "thumbnail",
124+
"format": "image/jpeg"
125+
}
126+
}
127+
128+
# Add the ingredient to the builder loading information from a source file.
129+
builder.add_ingredient_file(ingredient_json, "tests/fixtures/A.jpg")
130+
131+
# At this point we could archive or unarchive our Builder to continue later.
132+
# In this example we use a bytearray for the archive stream.
133+
# all ingredients and resources will be saved in the archive
134+
archive = io.BytesIO(bytearray())
135+
builder.to_archive(archive)
136+
archive.seek()
137+
builder = builder.from_archive(archive)
138+
139+
# Sign and add our manifest to a source file, writing it to an output file.
140+
# This returns the binary manifest data that could be uploaded to cloud storage.
141+
c2pa_data = builder.sign_file(signer, "tests/fixtures/A.jpg", "target/out.jpg")
142+
143+
except Exception as err:
144+
print(err)
145+
```
88146

89147
### Creating a manifest JSON definition file
90148

c2pa/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2024 Adobe. All rights reserved.
2+
# This file is licensed to you under the Apache License,
3+
# Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
4+
# or the MIT license (http://opensource.org/licenses/MIT),
5+
# at your option.
6+
7+
# Unless required by applicable law or agreed to in writing,
8+
# this software is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
10+
# implied. See the LICENSE-MIT and LICENSE-APACHE files for the
11+
# specific language governing permissions and limitations under
12+
# each license.
13+
14+
from .c2pa_api import Reader, Builder, create_signer, sign_ps256
15+
from .c2pa import Error, SigningAlg, sdk_version, version
16+
17+
__all__ = ['Reader', 'Builder', 'create_signer', 'sign_ps256', 'Error', 'SigningAlg', 'sdk_version', 'version']

c2pa/c2pa_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .c2pa_api import *

tests/c2pa_api.py renamed to c2pa/c2pa_api/c2pa_api.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
)
2222
sys.path.append(SOURCE_PATH)
2323

24-
import c2pa;
24+
import c2pa.c2pa as api
2525

26-
from c2pa import Error, SigningAlg, version, sdk_version
26+
#from c2pa import Error, SigningAlg, version, sdk_version
2727

2828
# This module provides a simple Python API for the C2PA library.
2929

@@ -32,9 +32,9 @@
3232
# It also supports writing resources to a stream or file.
3333
#
3434
# Example:
35-
# reader = c2pa_api.Reader("image/jpeg", open("test.jpg", "rb"))
35+
# reader = Reader("image/jpeg", open("test.jpg", "rb"))
3636
# json = reader.json()
37-
class Reader(c2pa.Reader):
37+
class Reader(api.Reader):
3838
def __init__(self, format, stream):
3939
super().__init__()
4040
self.from_stream(format, C2paStream(stream))
@@ -47,6 +47,17 @@ def from_file(cls, path: str, format=None):
4747
format = os.path.splitext(path)[1][1:]
4848
return cls(format, file)
4949

50+
def get_manifest(self, label):
51+
manifest_store = json.loads(self.json())
52+
return manifest_store["manifests"].get(label)
53+
return json.loads(self.json())
54+
def get_active_manifest(self):
55+
manifest_store = json.loads(self.json())
56+
active_label = manifest_store.get("active_manifest")
57+
if active_label:
58+
return manifest_store["manifests"].get(active_label)
59+
return None
60+
5061
def resource_to_stream(self, uri, stream) -> None:
5162
return super().resource_to_stream(uri, C2paStream(stream))
5263

@@ -72,11 +83,11 @@ def resource_to_file(self, uri, path) -> None:
7283
# "identifier": "thumbnail"
7384
# }
7485
# }
75-
# builder = c2pa_api.Builder(manifest)
86+
# builder = Builder(manifest)
7687
# builder.add_resource_file("thumbnail", "thumbnail.jpg")
7788
# builder.add_ingredient_file({"parentOf": true}, "B.jpg")
7889
# builder.sign_file(signer, "test.jpg", "signed.jpg")
79-
class Builder(c2pa.Builder):
90+
class Builder(api.Builder):
8091
def __init__(self, manifest):
8192
super().__init__()
8293
self.set_manifest(manifest)
@@ -126,19 +137,19 @@ def sign_file(self, signer, sourcePath, outputPath):
126137
# Implements a C2paStream given a stream handle
127138
# This is used to pass a file handle to the c2pa library
128139
# It is used by the Reader and Builder classes internally
129-
class C2paStream(c2pa.Stream):
140+
class C2paStream(api.Stream):
130141
def __init__(self, stream):
131142
self.stream = stream
132143

133144
def read_stream(self, length: int) -> bytes:
134145
#print("Reading " + str(length) + " bytes")
135146
return self.stream.read(length)
136147

137-
def seek_stream(self, pos: int, mode: c2pa.SeekMode) -> int:
148+
def seek_stream(self, pos: int, mode: api.SeekMode) -> int:
138149
whence = 0
139-
if mode is c2pa.SeekMode.CURRENT:
150+
if mode is api.SeekMode.CURRENT:
140151
whence = 1
141-
elif mode is c2pa.SeekMode.END:
152+
elif mode is api.SeekMode.END:
142153
whence = 2
143154
#print("Seeking to " + str(pos) + " with whence " + str(whence))
144155
return self.stream.seek(pos, whence)
@@ -151,12 +162,12 @@ def flush_stream(self) -> None:
151162
self.stream.flush()
152163

153164
# A shortcut method to open a C2paStream from a path/mode
154-
def open_file(path: str, mode: str) -> c2pa.Stream:
165+
def open_file(path: str, mode: str) -> api.Stream:
155166
return C2paStream(open(path, mode))
156167

157168
# Internal class to implement signer callbacks
158169
# We need this because the callback expects a class with a sign method
159-
class SignerCallback(c2pa.SignerCallback):
170+
class SignerCallback(api.SignerCallback):
160171
def __init__(self, callback):
161172
self.sign = callback
162173
super().__init__()
@@ -179,7 +190,7 @@ def __init__(self, callback):
179190
# signer = c2pa_api.create_signer(sign_ps256, "ps256", certs, "http://timestamp.digicert.com")
180191
#
181192
def create_signer(callback, alg, certs, timestamp_url=None):
182-
return c2pa.CallbackSigner(SignerCallback(callback), alg, certs, timestamp_url)
193+
return api.CallbackSigner(SignerCallback(callback), alg, certs, timestamp_url)
183194

184195

185196

tests/test_api.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
# specific language governing permissions and limitations under
1111
# each license.
1212

13-
1413
import json
1514
import pytest
1615
import tempfile
1716

18-
from c2pa_api import Builder, Error, Reader, SigningAlg, create_signer, sdk_version, sign_ps256, version
17+
from c2pa import Builder, Error, Reader, SigningAlg, create_signer, sdk_version, sign_ps256, version
1918

2019
# a little helper function to get a value from a nested dictionary
2120
from functools import reduce
@@ -60,12 +59,16 @@ def test_v2_read():
6059
#example of reading a manifest store from a file
6160
try:
6261
reader = Reader.from_file("tests/fixtures/C.jpg")
63-
jsonReport = reader.json()
64-
manifest_store = json.loads(jsonReport)
65-
manifest = manifest_store["manifests"][manifest_store["active_manifest"]]
62+
manifest = reader.get_active_manifest()
63+
if manifest is None:
64+
print("No active manifest found")
65+
exit(1)
66+
#jsonReport = reader.json()
67+
#manifest_store = json.loads(jsonReport)
68+
#manifest = manifest_store["manifests"][manifest_store["active_manifest"]]
6669
assert "make_test_images" in manifest["claim_generator"]
6770
assert manifest["title"]== "C.jpg"
68-
assert manifest,["format"] == "image/jpeg"
71+
assert manifest["format"] == "image/jpeg"
6972
# There should be no validation status errors
7073
assert manifest.get("validation_status") == None
7174
# read creative work assertion (author name)

tests/training.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import os
1717
import sys
1818

19-
from c2pa_api import Builder, Reader, create_signer, SigningAlg, sign_ps256, version
19+
from c2pa import *
2020

2121
# set up paths to the files we we are using
2222
PROJECT_PATH = os.getcwd()

tests/unit_tests.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import unittest
1818
from unittest.mock import mock_open, patch
1919

20-
from c2pa_api import Builder, Error, Reader, SigningAlg, create_signer, sdk_version, sign_ps256
20+
from c2pa import Builder, Error, Reader, SigningAlg, create_signer, sdk_version, sign_ps256
2121

2222
PROJECT_PATH = os.getcwd()
2323

@@ -26,7 +26,7 @@
2626
class TestC2paSdk(unittest.TestCase):
2727

2828
def test_version(self):
29-
self.assertIn("0.4.0", sdk_version())
29+
self.assertIn("0.5.0", sdk_version())
3030

3131

3232
class TestReader(unittest.TestCase):
@@ -94,7 +94,7 @@ def sign(data: bytes) -> bytes:
9494
def test_streams_build(self):
9595
with open(testPath, "rb") as file:
9696
builder = Builder(TestBuilder.manifestDefinition)
97-
output = byte_array = io.BytesIO(bytearray())
97+
output = io.BytesIO(bytearray())
9898
builder.sign(TestBuilder.signer, "image/jpeg", file, output)
9999
output.seek(0)
100100
reader = Reader("image/jpeg", output)

0 commit comments

Comments
 (0)