Add support for png_text metadata, allow to customize metadata for other backends. #7349

Merged
merged 21 commits into from Dec 25, 2016
Commits
+142 −19
Split
@@ -0,0 +1,20 @@
+Metadata savefig kwarg
+----------------------
+
+:func:`~matplotlib.pyplot.savefig` now accepts `metadata` as a keyword argument.
+It can be used to store key/value pairs in the image metadata.
+
+Supported formats and backends
+``````````````````````````````
+* 'png' with Agg backend
+* 'pdf' with PDF backend (see
+ :func:`~matplotlib.backends.backend_pdf.PdfFile.writeInfoDict` for a list of
+ supported keywords)
+* 'eps' and 'ps' with PS backend (only 'Creator' key is accepted)
+
+Example
+```````
+::
+
+ plt.savefig('test.png', metadata={'Software': 'My awesome software'})
+
@@ -26,8 +26,9 @@
import threading
import numpy as np
+from collections import OrderedDict
from math import radians, cos, sin
-from matplotlib import verbose, rcParams
+from matplotlib import verbose, rcParams, __version__
from matplotlib.backend_bases import (RendererBase, FigureManagerBase,
FigureCanvasBase)
from matplotlib.cbook import is_string_like, maxdict, restrict_dict
@@ -554,8 +555,16 @@ def print_png(self, filename_or_obj, *args, **kwargs):
else:
close = False
+ version_str = 'matplotlib version ' + __version__ + \
+ ', http://matplotlib.org/'
+ metadata = OrderedDict({'Software': version_str})
+ user_metadata = kwargs.pop("metadata", None)
+ if user_metadata is not None:
+ metadata.update(user_metadata)
+
try:
- _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi)
+ _png.write_png(renderer._renderer, filename_or_obj,
+ self.figure.dpi, metadata=metadata)
finally:
if close:
filename_or_obj.close()
@@ -428,7 +428,7 @@ def _flush(self):
class PdfFile(object):
"""PDF file object."""
- def __init__(self, filename):
+ def __init__(self, filename, metadata=None):
self.nextObject = 1 # next free object id
self.xrefTable = [[0, 65535, 'the zero object']]
self.passed_in_file_object = False
@@ -486,7 +486,9 @@ def __init__(self, filename):
'Creator': 'matplotlib %s, http://matplotlib.org' % __version__,
'Producer': 'matplotlib pdf backend%s' % revision,
'CreationDate': source_date
- }
+ }
+ if metadata is not None:
+ self.infoDict.update(metadata)
self.fontNames = {} # maps filenames to internal font names
self.nextFont = 1 # next free internal font name
@@ -2438,22 +2440,27 @@ class PdfPages(object):
"""
__slots__ = ('_file', 'keep_empty')
- def __init__(self, filename, keep_empty=True):
+ def __init__(self, filename, keep_empty=True, metadata=None):
"""
Create a new PdfPages object.
Parameters
----------
- filename: str
+ filename : str
Plots using :meth:`PdfPages.savefig` will be written to a file at
this location. The file is opened at once and any older file with
the same name is overwritten.
- keep_empty: bool, optional
+ keep_empty : bool, optional
If set to False, then empty pdf files will be deleted automatically
when closed.
+ metadata : dictionary, optional
+ Information dictionary object (see PDF reference section 10.2.1
+ 'Document Information Dictionary'), e.g.:
+ `{'Creator': 'My software', 'Author': 'Me',
+ 'Title': 'Awesome fig'}`
"""
- self._file = PdfFile(filename)
+ self._file = PdfFile(filename, metadata=metadata)
self.keep_empty = keep_empty
def __enter__(self):
@@ -2492,7 +2499,7 @@ def savefig(self, figure=None, **kwargs):
Parameters
----------
- figure: :class:`~matplotlib.figure.Figure` or int, optional
+ figure : :class:`~matplotlib.figure.Figure` or int, optional
Specifies what figure is saved to file. If not specified, the
active figure is saved. If a :class:`~matplotlib.figure.Figure`
instance is provided, this figure is saved. If an int is specified,
@@ -2556,7 +2563,7 @@ def print_pdf(self, filename, **kwargs):
if isinstance(filename, PdfPages):
file = filename._file
else:
- file = PdfFile(filename)
+ file = PdfFile(filename, metadata=kwargs.pop("metadata", None))
try:
file.newPage(width, height)
_bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)
@@ -965,7 +965,7 @@ def _print_ps(self, outfile, format, *args, **kwargs):
def _print_figure(self, outfile, format, dpi=72, facecolor='w', edgecolor='w',
orientation='portrait', isLandscape=False, papertype=None,
- **kwargs):
+ metadata=None, **kwargs):
"""
Render the figure to hardcopy. Set the figure patch face and
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',
If outfile is a file object, a stand-alone PostScript file is
written into this file object.
+
+ metadata must be a dictionary. Currently, only the value for
+ the key 'Creator' is used.
"""
isEPSF = format == 'eps'
passed_in_file_object = False
@@ -1059,13 +1062,18 @@ def write(self, *kl, **kwargs):
self.figure.set_facecolor(origfacecolor)
self.figure.set_edgecolor(origedgecolor)
+ # check for custom metadata
+ if metadata is not None and 'Creator' in metadata:
+ creator_str = metadata['Creator']
+ else:
+ creator_str = "matplotlib version " + __version__ + \
+ ", http://matplotlib.org/"
def print_figure_impl():
# write the PostScript headers
if isEPSF: print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
else: print("%!PS-Adobe-3.0", file=fh)
if title: print("%%Title: "+title, file=fh)
- print(("%%Creator: matplotlib version "
- +__version__+", http://matplotlib.org/"), file=fh)
+ print("%%Creator: " + creator_str, file=fh)
# get source date from SOURCE_DATE_EPOCH, if set
# See https://reproducible-builds.org/specs/source-date-epoch/
source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")
@@ -1189,12 +1197,15 @@ def do_nothing():
os.chmod(outfile, mode)
def _print_figure_tex(self, outfile, format, dpi, facecolor, edgecolor,
- orientation, isLandscape, papertype,
+ orientation, isLandscape, papertype, metadata=None,
**kwargs):
"""
If text.usetex is True in rc, a temporary pair of tex/eps files
are created to allow tex to manage the text layout via the PSFrags
package. These files are processed to yield the final ps or eps file.
+
+ metadata must be a dictionary. Currently, only the value for
+ the key 'Creator' is used.
"""
isEPSF = format == 'eps'
if is_string_like(outfile):
@@ -1249,14 +1260,20 @@ def write(self, *kl, **kwargs):
self.figure.set_facecolor(origfacecolor)
self.figure.set_edgecolor(origedgecolor)
+ # check for custom metadata
+ if metadata is not None and 'Creator' in metadata:
+ creator_str = metadata['Creator']
+ else:
+ creator_str = "matplotlib version " + __version__ + \
+ ", http://matplotlib.org/"
+
# write to a temp file, we'll move it to outfile when done
fd, tmpfile = mkstemp()
with io.open(fd, 'w', encoding='latin-1') as fh:
# write the Encapsulated PostScript headers
print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
if title: print("%%Title: "+title, file=fh)
- print(("%%Creator: matplotlib version "
- +__version__+", http://matplotlib.org/"), file=fh)
+ print("%%Creator: " + creator_str, file=fh)
# get source date from SOURCE_DATE_EPOCH, if set
# See https://reproducible-builds.org/specs/source-date-epoch/
source_date_epoch = os.getenv("SOURCE_DATE_EPOCH")
View
@@ -114,6 +114,24 @@ const char *Py_write_png__doc__ =
" If not provided, libpng will try to automatically determine the\n"
" best filter on a line-by-line basis.\n"
"\n"
+ "metadata : dictionary\n"
+ " The keyword-text pairs that are stored as comments in the image.\n"
+ " Keys must be shorter than 79 chars. The only supported encoding\n"
+ " for both keywords and values is Latin-1 (ISO 8859-1).\n"
+ " Examples given in the PNG Specification are:\n"
+ " - Title: Short (one line) title or caption for image\n"
+ " - Author: Name of image's creator\n"
+ " - Description: Description of image (possibly long)\n"
+ " - Copyright: Copyright notice\n"
+ " - Creation Time: Time of original image creation\n"
+ " (usually RFC 1123 format, see below)\n"
+ " - Software: Software used to create the image\n"
+ " - Disclaimer: Legal disclaimer\n"
+ " - Warning: Warning of nature of content\n"
+ " - Source: Device used to create the image\n"
+ " - Comment: Miscellaneous comment; conversion\n"
+ " from other image format\n"
+ "\n"
"Returns\n"
"-------\n"
"buffer : bytes or None\n"
@@ -124,25 +142,32 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
{
numpy::array_view<unsigned char, 3> buffer;
PyObject *filein;
+ PyObject *metadata = NULL;
+ PyObject *meta_key, *meta_val;
+ png_text *text;
+ Py_ssize_t pos = 0;
+ int meta_pos = 0;
+ Py_ssize_t meta_size;
double dpi = 0;
int compression = 6;
int filter = -1;
- const char *names[] = { "buffer", "file", "dpi", "compression", "filter", NULL };
+ const char *names[] = { "buffer", "file", "dpi", "compression", "filter", "metadata", NULL };
// We don't need strict contiguity, just for each row to be
// contiguous, and libpng has special handling for getting RGB out
// of RGBA, ARGB or BGR. But the simplest thing to do is to
// enforce contiguity using array_view::converter_contiguous.
if (!PyArg_ParseTupleAndKeywords(args,
kwds,
- "O&O|dii:write_png",
+ "O&O|diiO:write_png",
(char **)names,
&buffer.converter_contiguous,
&buffer,
&filein,
&dpi,
&compression,
- &filter)) {
+ &filter,
+ &metadata)) {
return NULL;
}
@@ -276,6 +301,51 @@ static PyObject *Py_write_png(PyObject *self, PyObject *args, PyObject *kwds)
png_set_pHYs(png_ptr, info_ptr, dots_per_meter, dots_per_meter, PNG_RESOLUTION_METER);
}
+#ifdef PNG_TEXT_SUPPORTED
+ // Save the metadata
+ if (metadata != NULL) {
+ meta_size = PyDict_Size(metadata);
+ text = new png_text[meta_size];
+
+ while (PyDict_Next(metadata, &pos, &meta_key, &meta_val)) {
+ text[meta_pos].compression = PNG_TEXT_COMPRESSION_NONE;
+#if PY3K
+ if (PyUnicode_Check(meta_key)) {
+ PyObject *temp_key = PyUnicode_AsEncodedString(meta_key, "latin_1", "strict");
+ if (temp_key != NULL) {
+ text[meta_pos].key = PyBytes_AsString(temp_key);
+ }
+ } else if (PyBytes_Check(meta_key)) {
+ text[meta_pos].key = PyBytes_AsString(meta_key);
+ } else {
+ char invalid_key[79];
+ sprintf(invalid_key,"INVALID KEY %d", meta_pos);
+ text[meta_pos].key = invalid_key;
+ }
+ if (PyUnicode_Check(meta_val)) {
+ PyObject *temp_val = PyUnicode_AsEncodedString(meta_val, "latin_1", "strict");
+ if (temp_val != NULL) {
+ text[meta_pos].text = PyBytes_AsString(temp_val);
+ }
+ } else if (PyBytes_Check(meta_val)) {
+ text[meta_pos].text = PyBytes_AsString(meta_val);
+ } else {
+ text[meta_pos].text = (char *)"Invalid value in metadata";
+ }
+#else
+ text[meta_pos].key = PyString_AsString(meta_key);
+ text[meta_pos].text = PyString_AsString(meta_val);
+#endif
+#ifdef PNG_iTXt_SUPPORTED
+ text[meta_pos].lang = NULL;
+#endif
+ meta_pos++;
+ }
+ png_set_text(png_ptr, info_ptr, text, meta_size);
+ delete[] text;
+ }
+#endif
+
sig_bit.alpha = 0;
switch (png_color_type) {
case PNG_COLOR_TYPE_GRAY: