From 2181f7eaa3f23260792dea172b0e423fb6dc247f Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 25 May 2015 22:24:01 +0200 Subject: [PATCH 1/3] optional user defined svg hash salt --- doc/users/whats_new/rcparams.rst | 5 +++++ lib/matplotlib/backends/backend_svg.py | 5 ++++- lib/matplotlib/rcsetup.py | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 doc/users/whats_new/rcparams.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/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 618ef5232856..483e0bba10fa 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -308,7 +308,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') diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index fa88462cbd6c..d00d40e75849 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1165,6 +1165,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_any], # set this when you want to generate hardcopy docstring 'docstring.hardcopy': [False, validate_bool], From cb5893de95b92f4bdba213dc64067a3042128d56 Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 16 May 2015 15:24:35 +0200 Subject: [PATCH 2/3] deterministic svg output by sorting dict items --- doc/api/backend_svg_api.rst | 7 ++++ doc/api/index_backend_api.rst | 1 + lib/matplotlib/axes/_base.py | 5 ++- lib/matplotlib/backends/backend_svg.py | 17 +++++----- lib/matplotlib/cbook.py | 14 ++++++++ lib/matplotlib/rcsetup.py | 12 ++++++- lib/matplotlib/tests/test_backend_svg.py | 42 ++++++++++++++++++++++++ matplotlibrc.template | 2 ++ 8 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 doc/api/backend_svg_api.rst 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/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 73320807d987..62c3b3584ef2 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -17,7 +17,7 @@ from matplotlib import cbook from matplotlib.cbook import (_check_1d, _string_to_bool, iterable, - index_of, get_label) + index_of, get_label, sorted_itervalues) from matplotlib import docstring import matplotlib.colors as mcolors import matplotlib.lines as mlines @@ -33,7 +33,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 @@ -3687,7 +3686,7 @@ def get_children(self): children.extend(self.lines) children.extend(self.texts) children.extend(self.artists) - children.extend(six.itervalues(self.spines)) + children.extend(sorted_itervalues(self.spines)) children.append(self.xaxis) children.append(self.yaxis) children.append(self.title) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 483e0bba10fa..3123d6466d7d 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -17,7 +17,8 @@ from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ FigureManagerBase, FigureCanvasBase from matplotlib.backends.backend_mixed import MixedModeRenderer -from matplotlib.cbook import is_string_like, is_writable_file_like, maxdict +from matplotlib.cbook import (is_string_like, is_writable_file_like, maxdict, + sorted_iteritems, sorted_itervalues) from matplotlib.colors import rgb2hex from matplotlib.figure import Figure from matplotlib.font_manager import findfont, FontProperties, get_font @@ -358,7 +359,7 @@ def _write_hatches(self): HATCH_SIZE = 72 writer = self.writer writer.start('defs') - for ((path, face, stroke), oid) in six.itervalues(self._hatchd): + for ((path, face, stroke), oid) in sorted_itervalues(self._hatchd): writer.start( 'pattern', id=oid, @@ -469,7 +470,7 @@ def _write_clips(self): return writer = self.writer writer.start('defs') - for clip, oid in six.itervalues(self._clipd): + for clip, oid in sorted_itervalues(self._clipd): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip @@ -488,7 +489,7 @@ def _write_svgfonts(self): writer = self.writer writer.start('defs') - for font_fname, chars in six.iteritems(self._fonts): + for font_fname, chars in sorted_iteritems(self._fonts): font = get_font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() @@ -829,7 +830,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')) @@ -917,7 +918,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if glyph_map_new: writer.start('defs') - for char_id, glyph_path in six.iteritems(glyph_map_new): + for char_id, glyph_path in sorted_iteritems(glyph_map_new): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) @@ -960,7 +961,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # used. if glyph_map_new: writer.start('defs') - for char_id, glyph_path in six.iteritems(glyph_map_new): + for char_id, glyph_path in sorted_iteritems(glyph_map_new): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): @@ -1104,7 +1105,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) - for style, chars in six.iteritems(spans): + for style, chars in sorted_iteritems(spans): chars.sort() same_y = True diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index b907ec01efbb..b06340477585 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2491,3 +2491,17 @@ def __exit__(self, exc_type, exc_value, traceback): os.rmdir(path) except OSError: pass + + +def sorted_iteritems(a): + """ + Iterate over the items of a dictionary in an order defined by the keys + """ + return ((k,v) for k,v in sorted(six.iteritems(a))) + + +def sorted_itervalues(a): + """ + Iterate over the values of a dictionary in an order defined by the keys + """ + return (v for k,v in sorted_iteritems(a)) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index d00d40e75849..72cf22a91530 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,7 +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_any], + '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/matplotlibrc.template b/matplotlibrc.template index 604b1036d911..b297f7d612bb 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 From 3289825038e72be82ff79c0fe094452c930fb82e Mon Sep 17 00:00:00 2001 From: klaus Date: Sun, 6 Dec 2015 13:48:10 +0100 Subject: [PATCH 3/3] OrderedDict instead of sorting --- lib/matplotlib/axes/_base.py | 7 ++++--- lib/matplotlib/backends/backend_svg.py | 18 +++++++++--------- lib/matplotlib/cbook.py | 14 -------------- lib/matplotlib/textpath.py | 3 ++- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 62c3b3584ef2..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 @@ -17,7 +18,7 @@ from matplotlib import cbook from matplotlib.cbook import (_check_1d, _string_to_bool, iterable, - index_of, get_label, sorted_itervalues) + index_of, get_label) from matplotlib import docstring import matplotlib.colors as mcolors import matplotlib.lines as mlines @@ -3686,7 +3687,7 @@ def get_children(self): children.extend(self.lines) children.extend(self.texts) children.extend(self.artists) - children.extend(sorted_itervalues(self.spines)) + children.extend(six.itervalues(self.spines)) children.append(self.xaxis) children.append(self.yaxis) children.append(self.title) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 3123d6466d7d..fa1cedf9c135 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 @@ -17,8 +18,7 @@ from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ FigureManagerBase, FigureCanvasBase from matplotlib.backends.backend_mixed import MixedModeRenderer -from matplotlib.cbook import (is_string_like, is_writable_file_like, maxdict, - sorted_iteritems, sorted_itervalues) +from matplotlib.cbook import is_string_like, is_writable_file_like, maxdict from matplotlib.colors import rgb2hex from matplotlib.figure import Figure from matplotlib.font_manager import findfont, FontProperties, get_font @@ -359,7 +359,7 @@ def _write_hatches(self): HATCH_SIZE = 72 writer = self.writer writer.start('defs') - for ((path, face, stroke), oid) in sorted_itervalues(self._hatchd): + for ((path, face, stroke), oid) in six.itervalues(self._hatchd): writer.start( 'pattern', id=oid, @@ -470,7 +470,7 @@ def _write_clips(self): return writer = self.writer writer.start('defs') - for clip, oid in sorted_itervalues(self._clipd): + for clip, oid in six.itervalues(self._clipd): writer.start('clipPath', id=oid) if len(clip) == 2: clippath, clippath_trans = clip @@ -489,7 +489,7 @@ def _write_svgfonts(self): writer = self.writer writer.start('defs') - for font_fname, chars in sorted_iteritems(self._fonts): + for font_fname, chars in six.iteritems(self._fonts): font = get_font(font_fname) font.set_size(72, 72) sfnt = font.get_sfnt() @@ -918,7 +918,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if glyph_map_new: writer.start('defs') - for char_id, glyph_path in sorted_iteritems(glyph_map_new): + for char_id, glyph_path in six.iteritems(glyph_map_new): path = Path(*glyph_path) path_data = self._convert_path(path, simplify=False) writer.element('path', id=char_id, d=path_data) @@ -961,7 +961,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): # used. if glyph_map_new: writer.start('defs') - for char_id, glyph_path in sorted_iteritems(glyph_map_new): + for char_id, glyph_path in six.iteritems(glyph_map_new): char_id = self._adjust_char_id(char_id) # Some characters are blank if not len(glyph_path[0]): @@ -1105,7 +1105,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): fontset = self._fonts.setdefault(font.fname, set()) fontset.add(thetext) - for style, chars in sorted_iteritems(spans): + for style, chars in six.iteritems(spans): chars.sort() same_y = True diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index b06340477585..b907ec01efbb 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2491,17 +2491,3 @@ def __exit__(self, exc_type, exc_value, traceback): os.rmdir(path) except OSError: pass - - -def sorted_iteritems(a): - """ - Iterate over the items of a dictionary in an order defined by the keys - """ - return ((k,v) for k,v in sorted(six.iteritems(a))) - - -def sorted_itervalues(a): - """ - Iterate over the values of a dictionary in an order defined by the keys - """ - return (v for k,v in sorted_iteritems(a)) 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):