Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ab98852

Browse files
authored
Merge pull request #7349 from data-exp-lab/png_metadata
ENH: Add support for png_text metadata, allow to customize metadata for other backends.
2 parents e73625e + 935e02f commit ab98852

File tree

5 files changed

+142
-19
lines changed

5 files changed

+142
-19
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Metadata savefig kwarg
2+
----------------------
3+
4+
:func:`~matplotlib.pyplot.savefig` now accepts `metadata` as a keyword argument.
5+
It can be used to store key/value pairs in the image metadata.
6+
7+
Supported formats and backends
8+
``````````````````````````````
9+
* 'png' with Agg backend
10+
* 'pdf' with PDF backend (see
11+
:func:`~matplotlib.backends.backend_pdf.PdfFile.writeInfoDict` for a list of
12+
supported keywords)
13+
* 'eps' and 'ps' with PS backend (only 'Creator' key is accepted)
14+
15+
Example
16+
```````
17+
::
18+
19+
plt.savefig('test.png', metadata={'Software': 'My awesome software'})
20+

lib/matplotlib/backends/backend_agg.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626

2727
import threading
2828
import numpy as np
29+
from collections import OrderedDict
2930
from math import radians, cos, sin
30-
from matplotlib import verbose, rcParams
31+
from matplotlib import verbose, rcParams, __version__
3132
from matplotlib.backend_bases import (RendererBase, FigureManagerBase,
3233
FigureCanvasBase)
3334
from matplotlib.cbook import is_string_like, maxdict, restrict_dict
@@ -554,8 +555,16 @@ def print_png(self, filename_or_obj, *args, **kwargs):
554555
else:
555556
close = False
556557

558+
version_str = 'matplotlib version ' + __version__ + \
559+
', http://matplotlib.org/'
560+
metadata = OrderedDict({'Software': version_str})
561+
user_metadata = kwargs.pop("metadata", None)
562+
if user_metadata is not None:
563+
metadata.update(user_metadata)
564+
557565
try:
558-
_png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi)
566+
_png.write_png(renderer._renderer, filename_or_obj,
567+
self.figure.dpi, metadata=metadata)
559568
finally:
560569
if close:
561570
filename_or_obj.close()

lib/matplotlib/backends/backend_pdf.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ def _flush(self):
428428
class PdfFile(object):
429429
"""PDF file object."""
430430

431-
def __init__(self, filename):
431+
def __init__(self, filename, metadata=None):
432432
self.nextObject = 1 # next free object id
433433
self.xrefTable = [[0, 65535, 'the zero object']]
434434
self.passed_in_file_object = False
@@ -486,7 +486,9 @@ def __init__(self, filename):
486486
'Creator': 'matplotlib %s, http://matplotlib.org' % __version__,
487487
'Producer': 'matplotlib pdf backend%s' % revision,
488488
'CreationDate': source_date
489-
}
489+
}
490+
if metadata is not None:
491+
self.infoDict.update(metadata)
490492

491493
self.fontNames = {} # maps filenames to internal font names
492494
self.nextFont = 1 # next free internal font name
@@ -2438,22 +2440,27 @@ class PdfPages(object):
24382440
"""
24392441
__slots__ = ('_file', 'keep_empty')
24402442

2441-
def __init__(self, filename, keep_empty=True):
2443+
def __init__(self, filename, keep_empty=True, metadata=None):
24422444
"""
24432445
Create a new PdfPages object.
24442446
24452447
Parameters
24462448
----------
24472449
2448-
filename: str
2450+
filename : str
24492451
Plots using :meth:`PdfPages.savefig` will be written to a file at
24502452
this location. The file is opened at once and any older file with
24512453
the same name is overwritten.
2452-
keep_empty: bool, optional
2454+
keep_empty : bool, optional
24532455
If set to False, then empty pdf files will be deleted automatically
24542456
when closed.
2457+
metadata : dictionary, optional
2458+
Information dictionary object (see PDF reference section 10.2.1
2459+
'Document Information Dictionary'), e.g.:
2460+
`{'Creator': 'My software', 'Author': 'Me',
2461+
'Title': 'Awesome fig'}`
24552462
"""
2456-
self._file = PdfFile(filename)
2463+
self._file = PdfFile(filename, metadata=metadata)
24572464
self.keep_empty = keep_empty
24582465

24592466
def __enter__(self):
@@ -2492,7 +2499,7 @@ def savefig(self, figure=None, **kwargs):
24922499
Parameters
24932500
----------
24942501
2495-
figure: :class:`~matplotlib.figure.Figure` or int, optional
2502+
figure : :class:`~matplotlib.figure.Figure` or int, optional
24962503
Specifies what figure is saved to file. If not specified, the
24972504
active figure is saved. If a :class:`~matplotlib.figure.Figure`
24982505
instance is provided, this figure is saved. If an int is specified,
@@ -2556,7 +2563,7 @@ def print_pdf(self, filename, **kwargs):
25562563
if isinstance(filename, PdfPages):
25572564
file = filename._file
25582565
else:
2559-
file = PdfFile(filename)
2566+
file = PdfFile(filename, metadata=kwargs.pop("metadata", None))
25602567
try:
25612568
file.newPage(width, height)
25622569
_bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)

lib/matplotlib/backends/backend_ps.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ def _print_ps(self, outfile, format, *args, **kwargs):
965965

966966
def _print_figure(self, outfile, format, dpi=72, facecolor='w', edgecolor='w',
967967
orientation='portrait', isLandscape=False, papertype=None,
968-
**kwargs):
968+
metadata=None, **kwargs):
969969
"""
970970
Render the figure to hardcopy. Set the figure patch face and
971971
edge colors. This is useful because some of the GUIs have a
@@ -978,6 +978,9 @@ def _print_figure(self, outfile, format, dpi=72, facecolor='w', edgecolor='w',
978978
979979
If outfile is a file object, a stand-alone PostScript file is
980980
written into this file object.
981+
982+
metadata must be a dictionary. Currently, only the value for
983+
the key 'Creator' is used.
981984
"""
982985
isEPSF = format == 'eps'
983986
passed_in_file_object = False
@@ -1059,13 +1062,18 @@ def write(self, *kl, **kwargs):
10591062
self.figure.set_facecolor(origfacecolor)
10601063
self.figure.set_edgecolor(origedgecolor)
10611064

1065+
# check for custom metadata
1066+
if metadata is not None and 'Creator' in metadata:
1067+
creator_str = metadata['Creator']
1068+
else:
1069+
creator_str = "matplotlib version " + __version__ + \
1070+
", http://matplotlib.org/"
10621071
def print_figure_impl():
10631072
# write the PostScript headers
10641073
if isEPSF: print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
10651074
else: print("%!PS-Adobe-3.0", file=fh)
10661075
if title: print("%%Title: "+title, file=fh)
1067-
print(("%%Creator: matplotlib version "
1068-
+__version__+", http://matplotlib.org/"), file=fh)
1076+
print("%%Creator: " + creator_str, file=fh)
10691077
# get source date from SOURCE_DATE_EPOCH, if set
10701078
# See https://reproducible-builds.org/specs/source-date-epoch/
10711079
source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")
@@ -1189,12 +1197,15 @@ def do_nothing():
11891197
os.chmod(outfile, mode)
11901198

11911199
def _print_figure_tex(self, outfile, format, dpi, facecolor, edgecolor,
1192-
orientation, isLandscape, papertype,
1200+
orientation, isLandscape, papertype, metadata=None,
11931201
**kwargs):
11941202
"""
11951203
If text.usetex is True in rc, a temporary pair of tex/eps files
11961204
are created to allow tex to manage the text layout via the PSFrags
11971205
package. These files are processed to yield the final ps or eps file.
1206+
1207+
metadata must be a dictionary. Currently, only the value for
1208+
the key 'Creator' is used.
11981209
"""
11991210
isEPSF = format == 'eps'
12001211
if is_string_like(outfile):
@@ -1249,14 +1260,20 @@ def write(self, *kl, **kwargs):
12491260
self.figure.set_facecolor(origfacecolor)
12501261
self.figure.set_edgecolor(origedgecolor)
12511262

1263+
# check for custom metadata
1264+
if metadata is not None and 'Creator' in metadata:
1265+
creator_str = metadata['Creator']
1266+
else:
1267+
creator_str = "matplotlib version " + __version__ + \
1268+
", http://matplotlib.org/"
1269+
12521270
# write to a temp file, we'll move it to outfile when done
12531271
fd, tmpfile = mkstemp()
12541272
with io.open(fd, 'w', encoding='latin-1') as fh:
12551273
# write the Encapsulated PostScript headers
12561274
print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
12571275
if title: print("%%Title: "+title, file=fh)
1258-
print(("%%Creator: matplotlib version "
1259-
+__version__+", http://matplotlib.org/"), file=fh)
1276+
print("%%Creator: " + creator_str, file=fh)
12601277
# get source date from SOURCE_DATE_EPOCH, if set
12611278
# See https://reproducible-builds.org/specs/source-date-epoch/
12621279
source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")

src/_png.cpp

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,24 @@ const char *Py_write_png__doc__ =
114114
" If not provided, libpng will try to automatically determine the\n"
115115
" best filter on a line-by-line basis.\n"
116116
"\n"
117+
"metadata : dictionary\n"
118+
" The keyword-text pairs that are stored as comments in the image.\n"
119+
" Keys must be shorter than 79 chars. The only supported encoding\n"
120+
" for both keywords and values is Latin-1 (ISO 8859-1).\n"
121+
" Examples given in the PNG Specification are:\n"
122+
" - Title: Short (one line) title or caption for image\n"
123+
" - Author: Name of image's creator\n"
124+
" - Description: Description of image (possibly long)\n"
125+
" - Copyright: Copyright notice\n"
126+
" - Creation Time: Time of original image creation\n"
127+
" (usually RFC 1123 format, see below)\n"
128+
" - Software: Software used to create the image\n"
129+
" - Disclaimer: Legal disclaimer\n"
130+
" - Warning: Warning of nature of content\n"
131+
" - Source: Device used to create the image\n"
132+
" - Comment: Miscellaneous comment; conversion\n"
133+
" from other image format\n"
134+
"\n"
117135
"Returns\n"
118136
"-------\n"
119137
"buffer : bytes or None\n"
@@ -124,25 +142,32 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
124142
{
125143
numpy::array_view<unsigned char, 3> buffer;
126144
PyObject *filein;
145+
PyObject *metadata = NULL;
146+
PyObject *meta_key, *meta_val;
147+
png_text *text;
148+
Py_ssize_t pos = 0;
149+
int meta_pos = 0;
150+
Py_ssize_t meta_size;
127151
double dpi = 0;
128152
int compression = 6;
129153
int filter = -1;
130-
const char *names[] = { "buffer", "file", "dpi", "compression", "filter", NULL };
154+
const char *names[] = { "buffer", "file", "dpi", "compression", "filter", "metadata", NULL };
131155

132156
// We don't need strict contiguity, just for each row to be
133157
// contiguous, and libpng has special handling for getting RGB out
134158
// of RGBA, ARGB or BGR. But the simplest thing to do is to
135159
// enforce contiguity using array_view::converter_contiguous.
136160
if (!PyArg_ParseTupleAndKeywords(args,
137161
kwds,
138-
"O&O|dii:write_png",
162+
"O&O|diiO:write_png",
139163
(char **)names,
140164
&buffer.converter_contiguous,
141165
&buffer,
142166
&filein,
143167
&dpi,
144168
&compression,
145-
&filter)) {
169+
&filter,
170+
&metadata)) {
146171
return NULL;
147172
}
148173

@@ -276,6 +301,51 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
276301
png_set_pHYs(png_ptr, info_ptr, dots_per_meter, dots_per_meter, PNG_RESOLUTION_METER);
277302
}
278303

304+
#ifdef PNG_TEXT_SUPPORTED
305+
// Save the metadata
306+
if (metadata != NULL) {
307+
meta_size = PyDict_Size(metadata);
308+
text = new png_text[meta_size];
309+
310+
while (PyDict_Next(metadata, &pos, &meta_key, &meta_val)) {
311+
text[meta_pos].compression = PNG_TEXT_COMPRESSION_NONE;
312+
#if PY3K
313+
if (PyUnicode_Check(meta_key)) {
314+
PyObject *temp_key = PyUnicode_AsEncodedString(meta_key, "latin_1", "strict");
315+
if (temp_key != NULL) {
316+
text[meta_pos].key = PyBytes_AsString(temp_key);
317+
}
318+
} else if (PyBytes_Check(meta_key)) {
319+
text[meta_pos].key = PyBytes_AsString(meta_key);
320+
} else {
321+
char invalid_key[79];
322+
sprintf(invalid_key,"INVALID KEY %d", meta_pos);
323+
text[meta_pos].key = invalid_key;
324+
}
325+
if (PyUnicode_Check(meta_val)) {
326+
PyObject *temp_val = PyUnicode_AsEncodedString(meta_val, "latin_1", "strict");
327+
if (temp_val != NULL) {
328+
text[meta_pos].text = PyBytes_AsString(temp_val);
329+
}
330+
} else if (PyBytes_Check(meta_val)) {
331+
text[meta_pos].text = PyBytes_AsString(meta_val);
332+
} else {
333+
text[meta_pos].text = (char *)"Invalid value in metadata";
334+
}
335+
#else
336+
text[meta_pos].key = PyString_AsString(meta_key);
337+
text[meta_pos].text = PyString_AsString(meta_val);
338+
#endif
339+
#ifdef PNG_iTXt_SUPPORTED
340+
text[meta_pos].lang = NULL;
341+
#endif
342+
meta_pos++;
343+
}
344+
png_set_text(png_ptr, info_ptr, text, meta_size);
345+
delete[] text;
346+
}
347+
#endif
348+
279349
sig_bit.alpha = 0;
280350
switch (png_color_type) {
281351
case PNG_COLOR_TYPE_GRAY:

0 commit comments

Comments
 (0)