@@ -135,6 +135,81 @@ 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, str]
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, str]
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
+ check_trapped = (lambda x : isinstance (x , Name ) and
191
+ x .name in ('True' , 'False' , 'Unknown' ))
192
+
193
+ keywords = {
194
+ 'Title' : is_string_like ,
195
+ 'Author' : is_string_like ,
196
+ 'Subject' : is_string_like ,
197
+ 'Keywords' : is_string_like ,
198
+ 'Creator' : is_string_like ,
199
+ 'Producer' : is_string_like ,
200
+ 'CreationDate' : is_date ,
201
+ 'ModDate' : is_date ,
202
+ 'Trapped' : check_trapped ,
203
+ }
204
+ for k in info :
205
+ if k not in keywords :
206
+ cbook ._warn_external (f'Unknown infodict keyword: { k } ' )
207
+ elif not keywords [k ](info [k ]):
208
+ cbook ._warn_external (f'Bad value for infodict keyword { k } ' )
209
+
210
+ return info
211
+
212
+
138
213
def pdfRepr (obj ):
139
214
"""Map Python objects to PDF syntax."""
140
215
@@ -503,24 +578,7 @@ def __init__(self, filename, metadata=None):
503
578
'Pages' : self .pagesObject }
504
579
self .writeObject (self .rootObject , root )
505
580
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 }
581
+ self .infoDict = _create_pdf_info_dict ('pdf' , metadata or {})
524
582
525
583
self .fontNames = {} # maps filenames to internal font names
526
584
self ._internal_font_seq = (Name (f'F{ i } ' ) for i in itertools .count (1 ))
@@ -1640,32 +1698,6 @@ def writeXref(self):
1640
1698
def writeInfoDict (self ):
1641
1699
"""Write out the info dictionary, checking it for good form"""
1642
1700
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
1701
self .infoObject = self .reserveObject ('info' )
1670
1702
self .writeObject (self .infoObject , self .infoDict )
1671
1703
0 commit comments