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

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Dec 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc6e367
Allow to store metadata in png files
Xarthisius Oct 25, 2016
5200aff
Ensure that metadata pointer is initially set to NULL
Xarthisius Oct 25, 2016
5a5bd96
Allow to pass custom infoDict to images created with pdf backend
Xarthisius Oct 25, 2016
d8cac23
Allow to pass custom metadata to png images created with agg backend
Xarthisius Oct 25, 2016
8305868
Allow to pass custom Creator to images created with ps backend
Xarthisius Oct 25, 2016
0d814e0
'Software' starts with capital letter as per PNG specification
Xarthisius Oct 25, 2016
112e38a
fix pep8 issue
Xarthisius Oct 25, 2016
0f006e3
Drop debug statements
Xarthisius Oct 25, 2016
12b0120
Preserve the default values of the metadata. Allow user to update if …
Xarthisius Oct 25, 2016
d75b068
Revert accidental changes and fix indentation
Xarthisius Oct 27, 2016
92bccd2
Handle unicode/bytes directly in the C extension
Xarthisius Oct 27, 2016
e174284
Use 'latin_1' encoding instead of ascii
Xarthisius Oct 29, 2016
af4c1d1
Fix numpydoc format issues
Xarthisius Oct 29, 2016
98b5587
Add example info dictionary
Xarthisius Oct 29, 2016
a2d2c37
Add a description for the 'metadata' keyword to the docstring
Xarthisius Oct 31, 2016
06ad9fb
Explicitly define 'metadata' kwarg in '_print_figure'
Xarthisius Oct 31, 2016
6916e77
Define 'metadata' as kwarg in _print_figure_tex for consistency
Xarthisius Oct 31, 2016
8bb0ae3
Use default value for invalid key instead of NULL
Xarthisius Oct 31, 2016
66f6e2d
Use OrderedDict for metadata
Xarthisius Nov 17, 2016
8897f0f
Add 'what is new' entry for metadata kwarg in matplotlib.pyplot.savefig
Xarthisius Nov 17, 2016
935e02f
Fix merge
Xarthisius Dec 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/users/whats_new/metadata_savefig_kwarg.rst
Original file line number Diff line number Diff line change
@@ -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'})

13 changes: 11 additions & 2 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
23 changes: 15 additions & 8 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't find this documentation particularly useful. You could you add at least some examples of metadata ?
Also, numpydoc format requires a space before the column : metadata : dictionary, …

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original parameters all don't have a space before the colon, and rendered fine, so I don't think that's a requirement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. The current documentation is not rendered properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, I missed that the bolding is wrong.

'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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
29 changes: 23 additions & 6 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down
76 changes: 73 additions & 3 deletions src/_png.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
}

Expand Down Expand Up @@ -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:
Expand Down