@@ -135,6 +135,105 @@ def _string_escape(match):
135135 assert False
136136
137137
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+ check_trapped = (lambda x : isinstance (x , Name ) and
191+ x .name in (b'True' , b'False' , b'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+
213+ def _datetime_to_pdf (d ):
214+ """
215+ Convert a datetime to a PDF string representing it.
216+
217+ Used for PDF and PGF.
218+ """
219+ r = d .strftime ('D:%Y%m%d%H%M%S' )
220+ z = d .utcoffset ()
221+ if z is not None :
222+ z = z .seconds
223+ else :
224+ if time .daylight :
225+ z = time .altzone
226+ else :
227+ z = time .timezone
228+ if z == 0 :
229+ r += 'Z'
230+ elif z < 0 :
231+ r += "+%02d'%02d'" % ((- z ) // 3600 , (- z ) % 3600 )
232+ else :
233+ r += "-%02d'%02d'" % (z // 3600 , z % 3600 )
234+ return r
235+
236+
138237def pdfRepr (obj ):
139238 """Map Python objects to PDF syntax."""
140239
@@ -199,22 +298,7 @@ def pdfRepr(obj):
199298
200299 # A date.
201300 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 )
301+ return pdfRepr (_datetime_to_pdf (obj ))
218302
219303 # A bounding box
220304 elif isinstance (obj , BboxBase ):
@@ -503,24 +587,7 @@ def __init__(self, filename, metadata=None):
503587 'Pages' : self .pagesObject }
504588 self .writeObject (self .rootObject , root )
505589
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 }
590+ self .infoDict = _create_pdf_info_dict ('pdf' , metadata or {})
524591
525592 self .fontNames = {} # maps filenames to internal font names
526593 self ._internal_font_seq = (Name (f'F{ i } ' ) for i in itertools .count (1 ))
@@ -1640,32 +1707,6 @@ def writeXref(self):
16401707 def writeInfoDict (self ):
16411708 """Write out the info dictionary, checking it for good form"""
16421709
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-
16691710 self .infoObject = self .reserveObject ('info' )
16701711 self .writeObject (self .infoObject , self .infoDict )
16711712
0 commit comments