Deterministic svg #5671

Merged
merged 4 commits into from Dec 14, 2015
@@ -0,0 +1,7 @@
+
+:mod:`matplotlib.backends.backend_svg`
+======================================
+
+.. automodule:: matplotlib.backends.backend_svg
+ :members:
+ :show-inheritance:
@@ -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
@@ -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.
@@ -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
@@ -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'))
View
@@ -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],
@@ -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)
@@ -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):
View
@@ -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