diff --git a/doc/api/backend_svg_api.rst b/doc/api/backend_svg_api.rst new file mode 100644 index 000000000000..399042482ea8 --- /dev/null +++ b/doc/api/backend_svg_api.rst @@ -0,0 +1,7 @@ + +:mod:`matplotlib.backends.backend_svg` +====================================== + +.. automodule:: matplotlib.backends.backend_svg + :members: + :show-inheritance: diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index 3874b814cc94..7153193529e4 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -12,6 +12,7 @@ backends backend_qt5agg_api.rst backend_wxagg_api.rst backend_pdf_api.rst + backend_svg_api.rst .. backend_webagg.rst dviread.rst type1font.rst diff --git a/doc/users/whats_new/rcparams.rst b/doc/users/whats_new/rcparams.rst new file mode 100644 index 000000000000..f66080d42b79 --- /dev/null +++ b/doc/users/whats_new/rcparams.rst @@ -0,0 +1,5 @@ +Added ``svg.hashsalt`` key to rcParams +``````````````````````````````````````` +If ``svg.hashsalt`` is ``None`` (which it is by default), the svg backend uses ``uuid4`` to generate the hash salt. +If it is not ``None``, it must be a string that is used as the hash salt instead of ``uuid4``. +This allows for deterministic SVG output. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 73320807d987..fa0a42f17c99 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1,10 +1,11 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from collections import OrderedDict + from matplotlib.externals import six from matplotlib.externals.six.moves import xrange -from collections import OrderedDict import itertools import warnings import math @@ -33,7 +34,6 @@ import matplotlib.image as mimage from matplotlib.offsetbox import OffsetBox from matplotlib.artist import allow_rasterization -from matplotlib.cbook import iterable, index_of from matplotlib.rcsetup import cycler rcParams = matplotlib.rcParams diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 648aff4c9581..691c7f0dee13 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1,12 +1,13 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from collections import OrderedDict + from matplotlib.externals import six from matplotlib.externals.six.moves import xrange from matplotlib.externals.six import unichr import os, base64, tempfile, gzip, io, sys, codecs, re -from collections import OrderedDict import numpy as np @@ -316,7 +317,10 @@ def _write_default_style(self): def _make_id(self, type, content): content = str(content) - salt = str(uuid.uuid4()) + if rcParams['svg.hashsalt'] is None: + salt = str(uuid.uuid4()) + else: + salt = rcParams['svg.hashsalt'] if six.PY3: content = content.encode('utf8') salt = salt.encode('utf8') @@ -840,7 +844,7 @@ def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): if rcParams['svg.image_inline']: bytesio = io.BytesIO() _png.write_png(np.array(im)[::-1], bytesio) - oid = oid or self._make_id('image', bytesio) + oid = oid or self._make_id('image', bytesio.getvalue()) attrib['xlink:href'] = ( "data:image/png;base64,\n" + base64.b64encode(bytesio.getvalue()).decode('ascii')) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 3ccf39ac9c42..883db3b0f7da 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -166,6 +166,16 @@ def validate_float_or_None(s): raise ValueError('Could not convert "%s" to float or None' % s) +def validate_string_or_None(s): + """convert s to string or raise""" + if s is None: + return None + try: + return six.text_type(s) + except ValueError: + raise ValueError('Could not convert "%s" to string' % s) + + def validate_dpi(s): """confirm s is string 'figure' or convert s to float or raise""" if s == 'figure': @@ -1165,6 +1175,7 @@ def validate_hist_bins(s): # True to save all characters as paths in the SVG 'svg.embed_char_paths': [True, deprecate_svg_embed_char_paths], 'svg.fonttype': ['path', validate_svg_fonttype], + 'svg.hashsalt': [None, validate_string_or_None], # set this when you want to generate hardcopy docstring 'docstring.hardcopy': [False, validate_bool], diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 9932250f0c5c..c0aacc55a888 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -119,6 +119,48 @@ def test_bold_font_output_with_none_fonttype(): ax.set_title('bold-title', fontweight='bold') +def _test_determinism(filename): + # This function is mostly copy&paste from "def test_visibility" + # To require no GUI, we use Figure and FigureCanvasSVG + # instead of plt.figure and fig.savefig + from matplotlib.figure import Figure + from matplotlib.backends.backend_svg import FigureCanvasSVG + from matplotlib import rc + rc('svg', hashsalt='asdf') + + fig = Figure() + ax = fig.add_subplot(111) + + x = np.linspace(0, 4 * np.pi, 50) + y = np.sin(x) + yerr = np.ones_like(y) + + a, b, c = ax.errorbar(x, y, yerr=yerr, fmt='ko') + for artist in b: + artist.set_visible(False) + + FigureCanvasSVG(fig).print_svg(filename) + + +@cleanup +def test_determinism(): + import os + import sys + from subprocess import check_call + from nose.tools import assert_equal + plots = [] + for i in range(3): + check_call([sys.executable, '-R', '-c', + 'from matplotlib.tests.test_backend_svg ' + 'import _test_determinism;' + '_test_determinism("determinism.svg")']) + with open('determinism.svg', 'rb') as fd: + plots.append(fd.read()) + os.unlink('determinism.svg') + for p in plots[1:]: + assert_equal(p, plots[0]) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 0a23401d3fba..f9de913bfaf3 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -3,6 +3,8 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +from collections import OrderedDict + from matplotlib.externals import six from matplotlib.externals.six.moves import zip @@ -20,7 +22,6 @@ from matplotlib.font_manager import FontProperties, get_font from matplotlib.transforms import Affine2D from matplotlib.externals.six.moves.urllib.parse import quote as urllib_quote -from collections import OrderedDict class TextToPath(object): diff --git a/matplotlibrc.template b/matplotlibrc.template index 5c9139067dee..45568b04ef33 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -505,6 +505,8 @@ backend : %(backend)s # 'path': Embed characters as paths -- supported by most SVG renderers # 'svgfont': Embed characters as SVG fonts -- supported only by Chrome, # Opera and Safari +#svg.hashsalt : None # if not None, use this string as hash salt + # instead of uuid4 # docstring params #docstring.hardcopy = False # set this when you want to generate hardcopy docstring