77from datetime import datetime
88from decimal import Decimal
99from tempfile import TemporaryDirectory
10- from typing import Any , Dict , List , Optional , Union
10+ from typing import Any , Dict , Optional , Union
1111
1212import requests
1313
@@ -41,8 +41,17 @@ def format_datetime(date: Optional[datetime], to_timestamp=False) -> Optional[Un
4141 return date .strftime ('%Y-%m-%dT%H:%M:%SZ' )
4242
4343
44- def format_geolocation (value : Optional [str ]) -> Optional [str ]:
45- return f'{ Decimal (value ):.12f} ' if value else None
44+ def format_geolocation (value : Optional [str ], is_latitude : bool ) -> Optional [str ]:
45+ if not value :
46+ return None
47+ d = Decimal (value )
48+ degrees = int (abs (d ))
49+ minutes = (abs (d ) - degrees ) * 60
50+ if is_latitude :
51+ direction = 'N' if d >= 0 else 'S'
52+ else :
53+ direction = 'E' if d >= 0 else 'W'
54+ return f'{ degrees } ,{ minutes :.4f} { direction } '
4655
4756
4857def c2patool_inject (
@@ -98,76 +107,77 @@ def create_assertion_asset_tree(
98107 }
99108
100109
101- def create_assertion_creative_work (
110+ def create_assertion_metadata (
102111 nid : str ,
103- creator_name : str ,
104112 date_created : Optional [datetime ] = None ,
105- location_created : Optional [str ] = None ,
113+ latitude : Optional [str ] = None ,
114+ longitude : Optional [str ] = None ,
115+ date_captured : Optional [datetime ] = None ,
106116):
107- data = {
108- '@context' : 'https://schema.org' ,
109- '@type' : 'CreativeWork' ,
110- 'url' : f'https://verify.numbersprotocol.io/asset-profile/{ nid } ' ,
111- 'identifier' : nid ,
117+ metadata = {
118+ '@context' : {
119+ 'dc' : 'http://purl.org/dc/elements/1.1/' ,
120+ 'Iptc4xmpCore' : 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/' ,
121+ 'Iptc4xmpExt' : 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/' ,
122+ 'exif' : 'http://ns.adobe.com/exif/1.0/' ,
123+ 'exifEX' : 'http://cipa.jp/exif/2.32/' ,
124+ 'tiff' : 'http://ns.adobe.com/tiff/1.0/' ,
125+ 'xmp' : 'http://ns.adobe.com/xap/1.0/'
126+ },
127+ 'dc:identifier' : nid ,
112128 }
113- if creator_name :
114- data ['author' ] = [
115- {
116- '@type' : 'Person' ,
117- 'name' : creator_name ,
118- }
119- ]
120129 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
130+ metadata ['dc:date' ] = date_created .strftime ('%Y-%m-%dT%H:%M:%SZ' )
131+ if latitude :
132+ metadata ['exif:GPSLatitude' ] = format_geolocation (latitude , True )
133+ if longitude :
134+ metadata ['exif:GPSLongitude' ] = format_geolocation (longitude , False )
135+ if date_captured :
136+ metadata ['exif:GPSTimeStamp' ] = date_captured .strftime ('%H:%M:%S' )
137+ metadata ['exif:DateTimeOriginal' ] = date_captured .strftime ('%Y:%m:%d %H:%M:%S' )
124138
125139 return {
126- 'label' : 'stds.schema-org.CreativeWork ' ,
127- 'data' : data
140+ 'label' : 'c2pa.metadata ' ,
141+ 'data' : metadata
128142 }
129143
130144
131145def create_c2pa_manifest (
132146 nid : str ,
133- creator_name : str ,
134147 creator_public_key : str ,
135148 asset_hash : str ,
136149 * ,
137150 date_created : Optional [datetime ] = None ,
138151 latitude : Optional [str ] = None ,
139152 longitude : Optional [str ] = None ,
140153 date_captured : Optional [datetime ] = None ,
154+ creator_name : Optional [str ] = None ,
141155 alg : str = 'es256' ,
142156 ta_url : str = 'http://timestamp.digicert.com' ,
143157 vendor : str = 'numbersprotocol' ,
144158 claim_generator_name : str = 'Numbers Protocol' ,
159+ claim_generator_version : Optional [str ] = None ,
145160 digital_source_type : Optional [str ] = None ,
146161 generated_by : Optional [str ] = None ,
147162 asset_tree_cid : Optional [str ] = None ,
148163 asset_tree_sha256 : Optional [str ] = None ,
149164 asset_tree_signature : Optional [str ] = None ,
150165 committer : Optional [str ] = None ,
151166):
152- location_created = (
153- f'{ format_geolocation (latitude )} , { format_geolocation (longitude )} '
154- if latitude and longitude else None
155- )
167+ claim_generator_info = {'name' : claim_generator_name }
168+ if claim_generator_version :
169+ claim_generator_info ['version' ] = claim_generator_version
156170
157171 manifest = {
158172 'alg' : alg ,
159173 'ta_url' : ta_url ,
160174 'vendor' : vendor ,
161175 'claim_generator' : format_claim_generator (claim_generator_name ),
162- 'claim_generator_info' : [
163- {
164- 'name' : claim_generator_name ,
165- },
166- ],
176+ 'claim_generator_info' : [claim_generator_info ],
167177 'title' : nid ,
168178 'assertions' : [
169- create_assertion_creative_work (
170- nid , creator_name , date_created , location_created ,
179+ create_assertion_metadata (
180+ nid , date_created , latitude , longitude , date_captured ,
171181 ),
172182 {
173183 'label' : 'c2pa.actions.v2' ,
@@ -186,26 +196,9 @@ def create_c2pa_manifest(
186196 'publicKey' : creator_public_key ,
187197 'mediaHash' : asset_hash ,
188198 'captureTimestamp' : format_datetime (date_captured , to_timestamp = True ),
199+ ** ({'creatorName' : creator_name } if creator_name else {}),
189200 }
190201 },
191- {
192- 'label' : 'stds.exif' ,
193- 'data' : {
194- '@context' : {
195- 'EXIF' : 'http://ns.adobe.com/EXIF/1.0/' ,
196- 'EXIFEX' : 'http://cipa.jp/EXIF/2.32/' ,
197- 'dc' : 'http://purl.org/dc/elements/1.1/' ,
198- 'rdf' : 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' ,
199- 'tiff' : 'http://ns.adobe.com/tiff/1.0/' ,
200- 'xmp' : 'http://ns.adobe.com/xap/1.0/'
201- },
202- 'EXIF:GPSLatitude' : format_geolocation (latitude ),
203- 'EXIF:GPSLongitude' : format_geolocation (longitude ),
204- "EXIF:GPSTimeStamp" : format_datetime (date_captured ),
205- 'EXIF:DateTimeOriginal' : format_datetime (date_captured ),
206- },
207- 'kind' : 'Json'
208- },
209202 ]
210203 }
211204 if assertion_asset_tree := create_assertion_asset_tree (
@@ -215,83 +208,48 @@ def create_c2pa_manifest(
215208 return manifest
216209
217210
218- def create_custom_c2pa_manifest (
219- * ,
220- alg : str = 'es256' ,
221- ta_url : str = 'http://timestamp.digicert.com' ,
222- vendor : str = 'numbersprotocol' ,
223- claim_generator : str = 'Numbers_Protocol' ,
224- title : Optional [str ] = None ,
225- author_type : str = 'Person' ,
226- author_credential : Optional [List ] = None ,
227- author_identifier : Optional [str ] = None ,
228- author_name : Optional [str ] = None ,
229- c2pa_actions : Optional [List ] = None ,
230- custom_assertions : Optional [List ] = None ,
231- ):
232- manifest = {
233- 'alg' : alg ,
234- 'ta_url' : ta_url ,
235- 'vendor' : vendor ,
236- 'claim_generator' : claim_generator ,
237- 'title' : title ,
238- 'assertions' : [
239- {
240- 'label' : 'stds.schema-org.CreativeWork' ,
241- 'data' : {
242- '@context' : 'https://schema.org' ,
243- '@type' : 'CreativeWork' ,
244- 'author' : [
245- {
246- '@type' : author_type ,
247- 'credential' : author_credential or [],
248- 'identifier' : author_identifier ,
249- 'name' : author_name ,
250- }
251- ],
252- }
253- },
254- ]
255- }
256- manifest = {k : v for k , v in manifest .items () if v is not None }
257- if c2pa_actions :
258- manifest ['assertions' ].append (
259- {
260- 'label' : 'c2pa.actions' ,
261- 'data' :
262- {
263- 'actions' : c2pa_actions ,
264- }
265- }
266- )
267- if custom_assertions :
268- manifest ['assertions' ] += custom_assertions
269- return manifest
270-
271-
272211def create_action_c2pa_opened (
273212 asset_hex_hash : str ,
274213 digital_source_type : Optional [str ] = None ,
275214 software_agent : Optional [str ] = None ,
276215) -> Dict [str , Any ]:
216+ """Create a c2pa.opened action with ingredient reference.
217+
218+ Args:
219+ asset_hex_hash: Hex-encoded SHA256 hash of the asset
220+ digital_source_type: Digital source type. Can be either:
221+ - Short form: 'trainedAlgorithmicMedia' (IPTC namespace auto-prepended)
222+ - Full URI: 'http://c2pa.org/digitalsourcetype/empty' (used as-is)
223+ software_agent: Name of the software that created/modified the asset
224+
225+ Returns:
226+ Action dictionary with ingredient reference for c2pa.actions.v2
227+ """
277228 base64_hash = base64 .b64encode (binascii .unhexlify (asset_hex_hash )).decode ()
229+
230+ # Create action with ingredients reference (plural, as array)
231+ # c2patool 0.26+ uses c2pa.ingredient.v3 label
278232 action = {
279233 'action' : 'c2pa.opened' ,
280234 'parameters' : {
281235 'ingredients' : [
282236 {
283- 'url' : 'self#jumbf=c2pa.assertions/c2pa.ingredient' ,
237+ 'url' : 'self#jumbf=c2pa.assertions/c2pa.ingredient.v3 ' ,
284238 'alg' : 'sha256' ,
285239 'hash' : base64_hash ,
286240 },
287241 ],
288242 },
289243 }
290244 if digital_source_type :
291- action ['digitalSourceType' ] = (
292- 'http://cv.iptc.org/newscodes/digitalsourcetype/'
293- f'{ digital_source_type } '
294- )
245+ # If full URI provided, use as-is; otherwise prepend IPTC namespace
246+ if digital_source_type .startswith ('http://' ):
247+ action ['digitalSourceType' ] = digital_source_type
248+ else :
249+ action ['digitalSourceType' ] = (
250+ 'http://cv.iptc.org/newscodes/digitalsourcetype/'
251+ f'{ digital_source_type } '
252+ )
295253 if software_agent :
296254 action ['softwareAgent' ] = {
297255 'name' : software_agent ,
0 commit comments