Skip to content

Commit 195e262

Browse files
authored
Merge pull request #3 from numbersprotocol/feature-better-align-c2pa
feat(numbers_c2pa): better align with C2PA 1.4
2 parents 71f5d07 + 1967cf7 commit 195e262

File tree

4 files changed

+132
-43
lines changed

4 files changed

+132
-43
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ if __name__ == '__main__':
4545
)
4646
inject_file(
4747
'examples/numbers.png',
48+
'examples/numbers-c2pa.png',
4849
manifest=manifest,
50+
parent_path='examples/numbers.png',
4951
private_key=private_key,
5052
sign_cert=sign_cert,
5153
)

examples/inject_file.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
sign_cert = f.read()
1010
manifest = create_c2pa_manifest(
1111
nid='bafkreicxvzt6xwmu6rrghe4bwixup5aqrw5abcskg5mpt2gnt2h7buwwzm', # nid of numbers.png
12+
creator_name='Tester',
1213
creator_public_key='0x2FBfE8F2bA00B255e60c220755040B597d09aFFa', # ethereum wallet address
1314
asset_hash='57ae67ebd994f462639381b22f47f4108dba008a4a3758f9e8cd9e8ff0d2d6cb', # sha256sum of numbers.png
1415
date_created=datetime.now(),
15-
location_created='123.123, 45.45',
16+
latitude='123.123',
17+
longitude='45.45',
1618
date_captured=None,
1719
digital_source_type='trainedAlgorithmicMedia',
1820
generated_by='Stable Diffusion',
@@ -21,6 +23,7 @@
2123
'examples/numbers.png',
2224
'examples/numbers-c2pa.png',
2325
manifest=manifest,
26+
parent_path='examples/numbers.png',
2427
private_key=private_key,
2528
sign_cert=sign_cert,
2629
)

examples/numbers-c2pa.png

50.5 KB
Loading

src/numbers_c2pa/core.py

Lines changed: 126 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import base64
2+
import binascii
13
import json
24
import mimetypes
35
import os
@@ -19,6 +21,18 @@ def _mimetype_to_ext(asset_mime_type: str):
1921
return ext
2022

2123

24+
def format_claim_generator(name: str) -> str:
25+
"""
26+
Claim generator must be underscore-separated Pascal case
27+
ex. Numbers_Protocol
28+
"""
29+
# Split the name into words based on spaces or other delimiters
30+
words = name.replace('-', ' ').replace('_', ' ').split()
31+
32+
# Capitalize each word and join with underscores
33+
return '_'.join(word.capitalize() for word in words)
34+
35+
2236
def format_datetime(date: Optional[datetime], to_timestamp=False) -> Optional[Union[str, int]]:
2337
if not date:
2438
return None
@@ -36,6 +50,8 @@ def c2patool_inject(
3650
manifest_path: str,
3751
output_path: str,
3852
force_overwrite: bool,
53+
*,
54+
parent_path: Optional[str] = None,
3955
private_key: Optional[str] = None,
4056
sign_cert: Optional[str] = None,
4157
):
@@ -46,6 +62,8 @@ def c2patool_inject(
4662
env_vars['C2PA_SIGN_CERT'] = sign_cert
4763
command = f"c2patool '{file_path}' -m '{manifest_path}' -o '{output_path}'"
4864

65+
if parent_path:
66+
command += f" -p '{parent_path}'"
4967
if force_overwrite:
5068
command += ' -f'
5169
try:
@@ -60,18 +78,70 @@ def c2patool_inject(
6078
raise UnknownError(e.stderr) from e
6179

6280

81+
def create_assertion_asset_tree(
82+
asset_tree_cid: Optional[str] = None,
83+
asset_tree_sha256: Optional[str] = None,
84+
asset_tree_signature: Optional[str] = None,
85+
committer: Optional[str] = None,
86+
) -> Optional[Dict[str, Any]]:
87+
if not asset_tree_cid or not asset_tree_sha256 or not asset_tree_signature or not committer:
88+
return None
89+
90+
return {
91+
'label': 'io.numbersprotocol.asset-tree',
92+
'data': {
93+
'assetTreeCid': asset_tree_cid,
94+
'assetTreeSha256': asset_tree_sha256,
95+
'assetTreeSignature': asset_tree_signature,
96+
'committer': committer,
97+
}
98+
}
99+
100+
101+
def create_assertion_creative_work(
102+
nid: str,
103+
creator_name: str,
104+
date_created: Optional[datetime] = None,
105+
location_created: Optional[str] = None,
106+
):
107+
data = {
108+
'@context': 'https://schema.org',
109+
'@type': 'CreativeWork',
110+
'url': f'https://verify.numbersprotocol.io/asset-profile/{nid}',
111+
'identifier': nid,
112+
}
113+
if creator_name:
114+
data['author'] = [
115+
{
116+
'@type': 'Person',
117+
'name': creator_name,
118+
}
119+
]
120+
if isinstance(date_created, datetime):
121+
data['dateCreated'] = date_created.strftime('%Y-%m-%dT%H:%M:%SZ')
122+
if location_created:
123+
data['locationCreated'] = location_created
124+
125+
return {
126+
'label': 'stds.schema-org.CreativeWork',
127+
'data': data
128+
}
129+
130+
63131
def create_c2pa_manifest(
64132
nid: str,
133+
creator_name: str,
65134
creator_public_key: str,
66135
asset_hash: str,
67-
date_created: datetime,
136+
*,
137+
date_created: Optional[datetime] = None,
68138
latitude: Optional[str] = None,
69139
longitude: Optional[str] = None,
70140
date_captured: Optional[datetime] = None,
71141
alg: str = 'es256',
72142
ta_url: str = 'http://timestamp.digicert.com',
73143
vendor: str = 'numbersprotocol',
74-
claim_generator: str = 'Numbers_Protocol',
144+
claim_generator_name: str = 'Numbers Protocol',
75145
digital_source_type: Optional[str] = None,
76146
generated_by: Optional[str] = None,
77147
asset_tree_cid: Optional[str] = None,
@@ -83,51 +153,34 @@ def create_c2pa_manifest(
83153
f'{format_geolocation(latitude)}, {format_geolocation(longitude)}'
84154
if latitude and longitude else None
85155
)
156+
86157
manifest = {
87158
'alg': alg,
88159
'ta_url': ta_url,
89160
'vendor': vendor,
90-
'claim_generator': claim_generator,
91-
'title': nid,
92-
'assertions': [
161+
'claim_generator': format_claim_generator(claim_generator_name),
162+
'claim_generator_info': [
93163
{
94-
'label': 'stds.schema-org.CreativeWork',
95-
'data': {
96-
'@context': 'https://schema.org',
97-
'@type': 'CreativeWork',
98-
'url': f'https://verify.numbersprotocol.io/asset-profile/{nid}',
99-
'author': [
100-
{
101-
'@type': 'Person',
102-
'name': creator_public_key,
103-
}
104-
],
105-
'dateCreated': date_created.strftime('%Y-%m-%dT%H:%M:%SZ'),
106-
'locationCreated': location_created,
107-
'identifier': nid,
108-
}
164+
'name': claim_generator_name,
109165
},
166+
],
167+
'title': nid,
168+
'assertions': [
169+
create_assertion_creative_work(
170+
nid, creator_name, date_created, location_created,
171+
),
110172
{
111-
'label': 'c2pa.actions',
173+
'label': 'c2pa.actions.v2',
112174
'data': {
113175
'actions': [
114-
{
115-
'action': 'c2pa.opened',
116-
}
176+
create_action_c2pa_opened(
177+
asset_hash, digital_source_type, generated_by,
178+
),
117179
],
118180
}
119181
},
120182
{
121-
'label': 'numbers.assetTree',
122-
'data': {
123-
'assetTreeCid': asset_tree_cid,
124-
'assetTreeSha256': asset_tree_sha256,
125-
'assetTreeSignature': asset_tree_signature,
126-
'committer': committer,
127-
}
128-
},
129-
{
130-
'label': 'numbers.integrity.json',
183+
'label': 'io.numbersprotocol.integrity',
131184
'data': {
132185
'nid': nid,
133186
'publicKey': creator_public_key,
@@ -155,18 +208,15 @@ def create_c2pa_manifest(
155208
},
156209
]
157210
}
158-
if digital_source_type:
159-
manifest['assertions'][1]['data']['actions'][0].update({
160-
'digitalSourceType': f'http://cv.iptc.org/newscodes/digitalsourcetype/{digital_source_type}',
161-
})
162-
if generated_by:
163-
manifest['assertions'][1]['data']['actions'][0].update({
164-
'softwareAgent': f'{generated_by}'
165-
})
211+
if assertion_asset_tree := create_assertion_asset_tree(
212+
asset_tree_cid, asset_tree_sha256, asset_tree_signature, committer,
213+
):
214+
manifest['assertions'].append(assertion_asset_tree)
166215
return manifest
167216

168217

169218
def create_custom_c2pa_manifest(
219+
*,
170220
alg: str = 'es256',
171221
ta_url: str = 'http://timestamp.digicert.com',
172222
vendor: str = 'numbersprotocol',
@@ -219,10 +269,41 @@ def create_custom_c2pa_manifest(
219269
return manifest
220270

221271

272+
def create_action_c2pa_opened(
273+
asset_hex_hash: str,
274+
digital_source_type: Optional[str] = None,
275+
software_agent: Optional[str] = None,
276+
) -> Dict[str, Any]:
277+
base64_hash = base64.b64encode(binascii.unhexlify(asset_hex_hash)).decode()
278+
action = {
279+
'action': 'c2pa.opened',
280+
'parameters': {
281+
'ingredients': [
282+
{
283+
'url': 'self#jumbf=c2pa.assertions/c2pa.ingredient',
284+
'alg': 'sha256',
285+
'hash': base64_hash,
286+
},
287+
],
288+
},
289+
}
290+
if digital_source_type:
291+
action['digitalSourceType'] = (
292+
'http://cv.iptc.org/newscodes/digitalsourcetype/'
293+
f'{digital_source_type}'
294+
)
295+
if software_agent:
296+
action['softwareAgent'] = {
297+
'name': software_agent,
298+
}
299+
return action
300+
301+
222302
def inject(
223303
asset_bytes: bytes,
224304
asset_mime_type: str,
225305
manifest: Dict,
306+
*,
226307
private_key: Optional[str] = None,
227308
sign_cert: Optional[str] = None,
228309
force_overwrite: bool = True,
@@ -258,6 +339,8 @@ def inject_file(
258339
asset_file: str,
259340
c2pa_output_file: str,
260341
manifest: Dict[str, Any],
342+
*,
343+
parent_path: Optional[str] = None,
261344
private_key: Optional[str] = None,
262345
sign_cert: Optional[str] = None,
263346
force_overwrite: bool = True,
@@ -290,6 +373,7 @@ def inject_file(
290373
manifest_file.name,
291374
c2pa_output_file,
292375
force_overwrite=force_overwrite,
376+
parent_path=parent_path,
293377
private_key=private_key,
294378
sign_cert=sign_cert,
295379
)

0 commit comments

Comments
 (0)