Skip to content

Deterministic svg #5671

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 4 commits into from
Dec 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions doc/api/backend_svg_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

:mod:`matplotlib.backends.backend_svg`
======================================

.. automodule:: matplotlib.backends.backend_svg
:members:
:show-inheritance:
1 change: 1 addition & 0 deletions doc/api/index_backend_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions doc/users/whats_new/rcparams.rst
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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'))
Expand Down
11 changes: 11 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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],
Expand Down
42 changes: 42 additions & 0 deletions lib/matplotlib/tests/test_backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 1 deletion lib/matplotlib/textpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions matplotlibrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down