Skip to content

Commit d31a156

Browse files
committed
Merge pull request #5671 from mdboom/deterministic-svg
Deterministic svg
1 parent a6f881a commit d31a156

File tree

9 files changed

+79
-6
lines changed

9 files changed

+79
-6
lines changed

doc/api/backend_svg_api.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
:mod:`matplotlib.backends.backend_svg`
3+
======================================
4+
5+
.. automodule:: matplotlib.backends.backend_svg
6+
:members:
7+
:show-inheritance:

doc/api/index_backend_api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ backends
1212
backend_qt5agg_api.rst
1313
backend_wxagg_api.rst
1414
backend_pdf_api.rst
15+
backend_svg_api.rst
1516
.. backend_webagg.rst
1617
dviread.rst
1718
type1font.rst

doc/users/whats_new/rcparams.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added ``svg.hashsalt`` key to rcParams
2+
```````````````````````````````````````
3+
If ``svg.hashsalt`` is ``None`` (which it is by default), the svg backend uses ``uuid4`` to generate the hash salt.
4+
If it is not ``None``, it must be a string that is used as the hash salt instead of ``uuid4``.
5+
This allows for deterministic SVG output.

lib/matplotlib/axes/_base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import (absolute_import, division, print_function,
22
unicode_literals)
33

4+
from collections import OrderedDict
5+
46
from matplotlib.externals import six
57
from matplotlib.externals.six.moves import xrange
68

7-
from collections import OrderedDict
89
import itertools
910
import warnings
1011
import math
@@ -33,7 +34,6 @@
3334
import matplotlib.image as mimage
3435
from matplotlib.offsetbox import OffsetBox
3536
from matplotlib.artist import allow_rasterization
36-
from matplotlib.cbook import iterable, index_of
3737
from matplotlib.rcsetup import cycler
3838

3939
rcParams = matplotlib.rcParams

lib/matplotlib/backends/backend_svg.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import (absolute_import, division, print_function,
22
unicode_literals)
33

4+
from collections import OrderedDict
5+
46
from matplotlib.externals import six
57
from matplotlib.externals.six.moves import xrange
68
from matplotlib.externals.six import unichr
79

810
import os, base64, tempfile, gzip, io, sys, codecs, re
9-
from collections import OrderedDict
1011

1112
import numpy as np
1213

@@ -316,7 +317,10 @@ def _write_default_style(self):
316317

317318
def _make_id(self, type, content):
318319
content = str(content)
319-
salt = str(uuid.uuid4())
320+
if rcParams['svg.hashsalt'] is None:
321+
salt = str(uuid.uuid4())
322+
else:
323+
salt = rcParams['svg.hashsalt']
320324
if six.PY3:
321325
content = content.encode('utf8')
322326
salt = salt.encode('utf8')
@@ -840,7 +844,7 @@ def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None):
840844
if rcParams['svg.image_inline']:
841845
bytesio = io.BytesIO()
842846
_png.write_png(np.array(im)[::-1], bytesio)
843-
oid = oid or self._make_id('image', bytesio)
847+
oid = oid or self._make_id('image', bytesio.getvalue())
844848
attrib['xlink:href'] = (
845849
"data:image/png;base64,\n" +
846850
base64.b64encode(bytesio.getvalue()).decode('ascii'))

lib/matplotlib/rcsetup.py

+11
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ def validate_float_or_None(s):
166166
raise ValueError('Could not convert "%s" to float or None' % s)
167167

168168

169+
def validate_string_or_None(s):
170+
"""convert s to string or raise"""
171+
if s is None:
172+
return None
173+
try:
174+
return six.text_type(s)
175+
except ValueError:
176+
raise ValueError('Could not convert "%s" to string' % s)
177+
178+
169179
def validate_dpi(s):
170180
"""confirm s is string 'figure' or convert s to float or raise"""
171181
if s == 'figure':
@@ -1165,6 +1175,7 @@ def validate_hist_bins(s):
11651175
# True to save all characters as paths in the SVG
11661176
'svg.embed_char_paths': [True, deprecate_svg_embed_char_paths],
11671177
'svg.fonttype': ['path', validate_svg_fonttype],
1178+
'svg.hashsalt': [None, validate_string_or_None],
11681179

11691180
# set this when you want to generate hardcopy docstring
11701181
'docstring.hardcopy': [False, validate_bool],

lib/matplotlib/tests/test_backend_svg.py

+42
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,48 @@ def test_bold_font_output_with_none_fonttype():
119119
ax.set_title('bold-title', fontweight='bold')
120120

121121

122+
def _test_determinism(filename):
123+
# This function is mostly copy&paste from "def test_visibility"
124+
# To require no GUI, we use Figure and FigureCanvasSVG
125+
# instead of plt.figure and fig.savefig
126+
from matplotlib.figure import Figure
127+
from matplotlib.backends.backend_svg import FigureCanvasSVG
128+
from matplotlib import rc
129+
rc('svg', hashsalt='asdf')
130+
131+
fig = Figure()
132+
ax = fig.add_subplot(111)
133+
134+
x = np.linspace(0, 4 * np.pi, 50)
135+
y = np.sin(x)
136+
yerr = np.ones_like(y)
137+
138+
a, b, c = ax.errorbar(x, y, yerr=yerr, fmt='ko')
139+
for artist in b:
140+
artist.set_visible(False)
141+
142+
FigureCanvasSVG(fig).print_svg(filename)
143+
144+
145+
@cleanup
146+
def test_determinism():
147+
import os
148+
import sys
149+
from subprocess import check_call
150+
from nose.tools import assert_equal
151+
plots = []
152+
for i in range(3):
153+
check_call([sys.executable, '-R', '-c',
154+
'from matplotlib.tests.test_backend_svg '
155+
'import _test_determinism;'
156+
'_test_determinism("determinism.svg")'])
157+
with open('determinism.svg', 'rb') as fd:
158+
plots.append(fd.read())
159+
os.unlink('determinism.svg')
160+
for p in plots[1:]:
161+
assert_equal(p, plots[0])
162+
163+
122164
if __name__ == '__main__':
123165
import nose
124166
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)

lib/matplotlib/textpath.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from __future__ import (absolute_import, division, print_function,
44
unicode_literals)
55

6+
from collections import OrderedDict
7+
68
from matplotlib.externals import six
79
from matplotlib.externals.six.moves import zip
810

@@ -20,7 +22,6 @@
2022
from matplotlib.font_manager import FontProperties, get_font
2123
from matplotlib.transforms import Affine2D
2224
from matplotlib.externals.six.moves.urllib.parse import quote as urllib_quote
23-
from collections import OrderedDict
2425

2526

2627
class TextToPath(object):

matplotlibrc.template

+2
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,8 @@ backend : %(backend)s
505505
# 'path': Embed characters as paths -- supported by most SVG renderers
506506
# 'svgfont': Embed characters as SVG fonts -- supported only by Chrome,
507507
# Opera and Safari
508+
#svg.hashsalt : None # if not None, use this string as hash salt
509+
# instead of uuid4
508510

509511
# docstring params
510512
#docstring.hardcopy = False # set this when you want to generate hardcopy docstring

0 commit comments

Comments
 (0)