@@ -135,6 +135,110 @@ def _string_escape(match):
135
135
assert False
136
136
137
137
138
+ def _create_pdf_info_dict (backend , metadata ):
139
+ """
140
+ Create a PDF infoDict based on user-supplied metadata.
141
+
142
+ A default ``Creator``, ``Producer``, and ``CreationDate`` are added, though
143
+ the user metadata may override it. The date may be the current time, or a
144
+ time set by the ``SOURCE_DATE_EPOCH`` environment variable.
145
+
146
+ Metadata is verified to have the correct keys and their expected types. Any
147
+ unknown keys/types will raise a warning.
148
+
149
+ Parameters
150
+ ----------
151
+ backend : str
152
+ The name of the backend to use in the Producer value.
153
+ metadata : Dict[str, Union[str, datetime, Name]]
154
+ A dictionary of metadata supplied by the user with information
155
+ following the PDF specification, also defined in
156
+ `~.backend_pdf.PdfPages` below.
157
+
158
+ If any value is *None*, then the key will be removed. This can be used
159
+ to remove any pre-defined values.
160
+
161
+ Returns
162
+ -------
163
+ Dict[str, Union[str, datetime, Name]]
164
+ A validated dictionary of metadata.
165
+ """
166
+
167
+ # get source date from SOURCE_DATE_EPOCH, if set
168
+ # See https://reproducible-builds.org/specs/source-date-epoch/
169
+ source_date_epoch = os .getenv ("SOURCE_DATE_EPOCH" )
170
+ if source_date_epoch :
171
+ source_date = datetime .utcfromtimestamp (int (source_date_epoch ))
172
+ source_date = source_date .replace (tzinfo = UTC )
173
+ else :
174
+ source_date = datetime .today ()
175
+
176
+ info = {
177
+ 'Creator' : f'Matplotlib v{ mpl .__version__ } , https://matplotlib.org' ,
178
+ 'Producer' : f'Matplotlib { backend } backend v{ mpl .__version__ } ' ,
179
+ 'CreationDate' : source_date ,
180
+ ** metadata
181
+ }
182
+ info = {k : v for (k , v ) in info .items () if v is not None }
183
+
184
+ def is_string_like (x ):
185
+ return isinstance (x , str )
186
+
187
+ def is_date (x ):
188
+ return isinstance (x , datetime )
189
+
190
+ def check_trapped (x ):
191
+ if isinstance (x , Name ):
192
+ return x .name in (b'True' , b'False' , b'Unknown' )
193
+ else :
194
+ return x in ('True' , 'False' , 'Unknown' )
195
+
196
+ keywords = {
197
+ 'Title' : is_string_like ,
198
+ 'Author' : is_string_like ,
199
+ 'Subject' : is_string_like ,
200
+ 'Keywords' : is_string_like ,
201
+ 'Creator' : is_string_like ,
202
+ 'Producer' : is_string_like ,
203
+ 'CreationDate' : is_date ,
204
+ 'ModDate' : is_date ,
205
+ 'Trapped' : check_trapped ,
206
+ }
207
+ for k in info :
208
+ if k not in keywords :
209
+ cbook ._warn_external (f'Unknown infodict keyword: { k } ' )
210
+ elif not keywords [k ](info [k ]):
211
+ cbook ._warn_external (f'Bad value for infodict keyword { k } ' )
212
+ if 'Trapped' in info :
213
+ info ['Trapped' ] = Name (info ['Trapped' ])
214
+
215
+ return info
216
+
217
+
218
+ def _datetime_to_pdf (d ):
219
+ """
220
+ Convert a datetime to a PDF string representing it.
221
+
222
+ Used for PDF and PGF.
223
+ """
224
+ r = d .strftime ('D:%Y%m%d%H%M%S' )
225
+ z = d .utcoffset ()
226
+ if z is not None :
227
+ z = z .seconds
228
+ else :
229
+ if time .daylight :
230
+ z = time .altzone
231
+ else :
232
+ z = time .timezone
233
+ if z == 0 :
234
+ r += 'Z'
235
+ elif z < 0 :
236
+ r += "+%02d'%02d'" % ((- z ) // 3600 , (- z ) % 3600 )
237
+ else :
238
+ r += "-%02d'%02d'" % (z // 3600 , z % 3600 )
239
+ return r
240
+
241
+
138
242
def pdfRepr (obj ):
139
243
"""Map Python objects to PDF syntax."""
140
244
@@ -199,22 +303,7 @@ def pdfRepr(obj):
199
303
200
304
# A date.
201
305
elif isinstance (obj , datetime ):
202
- r = obj .strftime ('D:%Y%m%d%H%M%S' )
203
- z = obj .utcoffset ()
204
- if z is not None :
205
- z = z .seconds
206
- else :
207
- if time .daylight :
208
- z = time .altzone
209
- else :
210
- z = time .timezone
211
- if z == 0 :
212
- r += 'Z'
213
- elif z < 0 :
214
- r += "+%02d'%02d'" % ((- z ) // 3600 , (- z ) % 3600 )
215
- else :
216
- r += "-%02d'%02d'" % (z // 3600 , z % 3600 )
217
- return pdfRepr (r )
306
+ return pdfRepr (_datetime_to_pdf (obj ))
218
307
219
308
# A bounding box
220
309
elif isinstance (obj , BboxBase ):
@@ -503,24 +592,7 @@ def __init__(self, filename, metadata=None):
503
592
'Pages' : self .pagesObject }
504
593
self .writeObject (self .rootObject , root )
505
594
506
- # get source date from SOURCE_DATE_EPOCH, if set
507
- # See https://reproducible-builds.org/specs/source-date-epoch/
508
- source_date_epoch = os .getenv ("SOURCE_DATE_EPOCH" )
509
- if source_date_epoch :
510
- source_date = datetime .utcfromtimestamp (int (source_date_epoch ))
511
- source_date = source_date .replace (tzinfo = UTC )
512
- else :
513
- source_date = datetime .today ()
514
-
515
- self .infoDict = {
516
- 'Creator' : f'matplotlib { mpl .__version__ } , http://matplotlib.org' ,
517
- 'Producer' : f'matplotlib pdf backend { mpl .__version__ } ' ,
518
- 'CreationDate' : source_date
519
- }
520
- if metadata is not None :
521
- self .infoDict .update (metadata )
522
- self .infoDict = {k : v for (k , v ) in self .infoDict .items ()
523
- if v is not None }
595
+ self .infoDict = _create_pdf_info_dict ('pdf' , metadata or {})
524
596
525
597
self .fontNames = {} # maps filenames to internal font names
526
598
self ._internal_font_seq = (Name (f'F{ i } ' ) for i in itertools .count (1 ))
@@ -1640,32 +1712,6 @@ def writeXref(self):
1640
1712
def writeInfoDict (self ):
1641
1713
"""Write out the info dictionary, checking it for good form"""
1642
1714
1643
- def is_string_like (x ):
1644
- return isinstance (x , str )
1645
-
1646
- def is_date (x ):
1647
- return isinstance (x , datetime )
1648
-
1649
- check_trapped = (lambda x : isinstance (x , Name ) and
1650
- x .name in ('True' , 'False' , 'Unknown' ))
1651
-
1652
- keywords = {'Title' : is_string_like ,
1653
- 'Author' : is_string_like ,
1654
- 'Subject' : is_string_like ,
1655
- 'Keywords' : is_string_like ,
1656
- 'Creator' : is_string_like ,
1657
- 'Producer' : is_string_like ,
1658
- 'CreationDate' : is_date ,
1659
- 'ModDate' : is_date ,
1660
- 'Trapped' : check_trapped }
1661
- for k in self .infoDict :
1662
- if k not in keywords :
1663
- cbook ._warn_external ('Unknown infodict keyword: %s' % k )
1664
- else :
1665
- if not keywords [k ](self .infoDict [k ]):
1666
- cbook ._warn_external (
1667
- 'Bad value for infodict keyword %s' % k )
1668
-
1669
1715
self .infoObject = self .reserveObject ('info' )
1670
1716
self .writeObject (self .infoObject , self .infoDict )
1671
1717
0 commit comments