Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1172 lines (1009 sloc) 42.453 kB
from __future__ import division
import os, base64, tempfile, urllib, gzip, io, sys, codecs
import numpy as np
try:
from hashlib import md5
except ImportError:
from md5 import md5 #Deprecated in 2.5
from matplotlib import verbose, __version__, rcParams
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.colors import rgb2hex
from matplotlib.figure import Figure
from matplotlib.font_manager import findfont, FontProperties
from matplotlib.ft2font import FT2Font, KERNING_DEFAULT, LOAD_NO_HINTING
from matplotlib.mathtext import MathTextParser
from matplotlib.path import Path
from matplotlib import _path
from matplotlib.transforms import Affine2D, Affine2DBase
from matplotlib import _png
from xml.sax.saxutils import escape as escape_xml_text
backend_version = __version__
# ----------------------------------------------------------------------
# SimpleXMLWriter class
#
# Based on an original by Fredrik Lundh, but modified here to:
# 1. Support modern Python idioms
# 2. Remove encoding support (it's handled by the file writer instead)
# 3. Support proper indentation
# 4. Minify things a little bit
# --------------------------------------------------------------------
# The SimpleXMLWriter module is
#
# Copyright (c) 2001-2004 by Fredrik Lundh
#
# By obtaining, using, and/or copying this software and/or its
# associated documentation, you agree that you have read, understood,
# and will comply with the following terms and conditions:
#
# Permission to use, copy, modify, and distribute this software and
# its associated documentation for any purpose and without fee is
# hereby granted, provided that the above copyright notice appears in
# all copies, and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# Secret Labs AB or the author not be used in advertising or publicity
# pertaining to distribution of the software without specific, written
# prior permission.
#
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.
# --------------------------------------------------------------------
def escape_cdata(s):
s = s.replace(u"&", u"&")
s = s.replace(u"<", u"&lt;")
s = s.replace(u">", u"&gt;")
return s
def escape_attrib(s):
s = s.replace(u"&", u"&amp;")
s = s.replace(u"'", u"&apos;")
s = s.replace(u"\"", u"&quot;")
s = s.replace(u"<", u"&lt;")
s = s.replace(u">", u"&gt;")
return s
##
# XML writer class.
#
# @param file A file or file-like object. This object must implement
# a <b>write</b> method that takes an 8-bit string.
class XMLWriter:
def __init__(self, file):
self.__write = file.write
if hasattr(file, "flush"):
self.flush = file.flush
self.__open = 0 # true if start tag is open
self.__tags = []
self.__data = []
self.__indentation = u" " * 64
def __flush(self, indent=True):
# flush internal buffers
if self.__open:
if indent:
self.__write(u">\n")
else:
self.__write(u">")
self.__open = 0
if self.__data:
data = u''.join(self.__data)
self.__write(escape_cdata(data))
self.__data = []
## Opens a new element. Attributes can be given as keyword
# arguments, or as a string/string dictionary. The method returns
# an opaque identifier that can be passed to the <b>close</b>
# method, to close all open elements up to and including this one.
#
# @param tag Element tag.
# @param attrib Attribute dictionary. Alternatively, attributes
# can be given as keyword arguments.
# @return An element identifier.
def start(self, tag, attrib={}, **extra):
self.__flush()
tag = escape_cdata(tag)
self.__data = []
self.__tags.append(tag)
self.__write(self.__indentation[:len(self.__tags) - 1])
self.__write(u"<%s" % tag)
if attrib or extra:
attrib = attrib.copy()
attrib.update(extra)
attrib = attrib.items()
attrib.sort()
for k, v in attrib:
if not v == '':
k = escape_cdata(k)
v = escape_attrib(v)
self.__write(u" %s=\"%s\"" % (k, v))
self.__open = 1
return len(self.__tags)-1
##
# Adds a comment to the output stream.
#
# @param comment Comment text, as a Unicode string.
def comment(self, comment):
self.__flush()
self.__write(self.__indentation[:len(self.__tags)])
self.__write(u"<!-- %s -->\n" % escape_cdata(comment))
##
# Adds character data to the output stream.
#
# @param text Character data, as a Unicode string.
def data(self, text):
self.__data.append(text)
##
# Closes the current element (opened by the most recent call to
# <b>start</b>).
#
# @param tag Element tag. If given, the tag must match the start
# tag. If omitted, the current element is closed.
def end(self, tag=None, indent=True):
if tag:
assert self.__tags, "unbalanced end(%s)" % tag
assert escape_cdata(tag) == self.__tags[-1],\
"expected end(%s), got %s" % (self.__tags[-1], tag)
else:
assert self.__tags, "unbalanced end()"
tag = self.__tags.pop()
if self.__data:
self.__flush(indent)
elif self.__open:
self.__open = 0
self.__write(u"/>\n")
return
if indent:
self.__write(self.__indentation[:len(self.__tags)])
self.__write(u"</%s>\n" % tag)
##
# Closes open elements, up to (and including) the element identified
# by the given identifier.
#
# @param id Element identifier, as returned by the <b>start</b> method.
def close(self, id):
while len(self.__tags) > id:
self.end()
##
# Adds an entire element. This is the same as calling <b>start</b>,
# <b>data</b>, and <b>end</b> in sequence. The <b>text</b> argument
# can be omitted.
def element(self, tag, text=None, attrib={}, **extra):
apply(self.start, (tag, attrib), extra)
if text:
self.data(text)
self.end(indent=False)
##
# Flushes the output stream.
def flush(self):
pass # replaced by the constructor
# ----------------------------------------------------------------------
def generate_transform(transform_list=[]):
if len(transform_list):
output = io.StringIO()
for type, value in transform_list:
if type == u'scale' and (value == (1.0,) or value == (1.0, 1.0)):
continue
if type == u'translate' and value == (0.0, 0.0):
continue
if type == u'rotate' and value == (0.0,):
continue
if type == u'matrix' and isinstance(value, Affine2DBase):
value = value.to_values()
output.write(u'%s(%s)' % (type, ' '.join(str(x) for x in value)))
return output.getvalue()
return ''
def generate_css(attrib={}):
if attrib:
output = io.StringIO()
attrib = attrib.items()
attrib.sort()
for k, v in attrib:
k = escape_attrib(k)
v = escape_attrib(v)
output.write(u"%s:%s;" % (k, v))
return output.getvalue()
return ''
_capstyle_d = {'projecting' : u'square', 'butt' : u'butt', 'round': u'round',}
class RendererSVG(RendererBase):
FONT_SCALE = 100.0
fontd = maxdict(50)
def __init__(self, width, height, svgwriter, basename=None):
self.width = width
self.height = height
self.writer = XMLWriter(svgwriter)
self._groupd = {}
if not rcParams['svg.image_inline']:
assert basename is not None
self.basename = basename
self._imaged = {}
self._clipd = {}
self._char_defs = {}
self._markers = {}
self._path_collection_id = 0
self._imaged = {}
self._hatchd = {}
self._has_gouraud = False
self._n_gradients = 0
self._fonts = {}
self.mathtext_parser = MathTextParser('SVG')
RendererBase.__init__(self)
self._glyph_map = dict()
svgwriter.write(svgProlog)
self._start_id = self.writer.start(
u'svg',
width=u'%ipt' % width, height='%ipt' % height,
viewBox=u'0 0 %i %i' % (width, height),
xmlns=u"http://www.w3.org/2000/svg",
version=u"1.1",
attrib={u'xmlns:xlink': u"http://www.w3.org/1999/xlink"})
self._write_default_style()
def finalize(self):
self._write_clips()
self._write_hatches()
self._write_svgfonts()
self.writer.close(self._start_id)
self.writer.flush()
def _write_default_style(self):
writer = self.writer
default_style = generate_css({
u'stroke-linejoin': u'round',
u'stroke-linecap': u'square'})
writer.start(u'defs')
writer.start(u'style', type=u'text/css')
writer.data(u'*{%s}\n' % default_style)
writer.end(u'style')
writer.end(u'defs')
def _make_id(self, type, content):
content = str(content)
if sys.version_info[0] >= 3:
content = content.encode('utf8')
return u'%s%s' % (type, md5(content).hexdigest()[:10])
def _make_flip_transform(self, transform):
return (transform +
Affine2D()
.scale(1.0, -1.0)
.translate(0.0, self.height))
def _get_font(self, prop):
key = hash(prop)
font = self.fontd.get(key)
if font is None:
fname = findfont(prop)
font = self.fontd.get(fname)
if font is None:
font = FT2Font(str(fname))
self.fontd[fname] = font
self.fontd[key] = font
font.clear()
size = prop.get_size_in_points()
font.set_size(size, 72.0)
return font
def _get_hatch(self, gc, rgbFace):
"""
Create a new hatch pattern
"""
if rgbFace is not None:
rgbFace = tuple(rgbFace)
edge = gc.get_rgb()
if edge is not None:
edge = tuple(edge)
dictkey = (gc.get_hatch(), rgbFace, edge)
oid = self._hatchd.get(dictkey)
if oid is None:
oid = self._make_id(u'h', dictkey)
self._hatchd[dictkey] = ((gc.get_hatch_path(), rgbFace, edge), oid)
else:
_, oid = oid
return oid
def _write_hatches(self):
if not len(self._hatchd):
return
HATCH_SIZE = 72
writer = self.writer
writer.start('defs')
for ((path, face, stroke), oid) in self._hatchd.values():
writer.start(
u'pattern',
id=oid,
patternUnits=u"userSpaceOnUse",
x=u"0", y=u"0", width=unicode(HATCH_SIZE), height=unicode(HATCH_SIZE))
path_data = self._convert_path(
path,
Affine2D().scale(HATCH_SIZE).scale(1.0, -1.0).translate(0, HATCH_SIZE),
simplify=False)
if face is None:
fill = u'none'
else:
fill = rgb2hex(face)
writer.element(
u'rect',
x=u"0", y=u"0", width=unicode(HATCH_SIZE+1), height=unicode(HATCH_SIZE+1),
fill=fill)
writer.element(
u'path',
d=path_data,
style=generate_css({
u'fill': rgb2hex(stroke),
u'stroke': rgb2hex(stroke),
u'stroke-width': u'1.0',
u'stroke-linecap': u'butt',
u'stroke-linejoin': u'miter'
})
)
writer.end(u'pattern')
writer.end(u'defs')
def _get_style_dict(self, gc, rgbFace):
"""
return the style string. style is generated from the
GraphicsContext and rgbFace
"""
attrib = {}
if gc.get_hatch() is not None:
attrib[u'fill'] = u"url(#%s)" % self._get_hatch(gc, rgbFace)
else:
if rgbFace is None:
attrib[u'fill'] = u'none'
elif tuple(rgbFace[:3]) != (0, 0, 0):
attrib[u'fill'] = rgb2hex(rgbFace)
if gc.get_alpha() != 1.0:
attrib[u'opacity'] = str(gc.get_alpha())
offset, seq = gc.get_dashes()
if seq is not None:
attrib[u'stroke-dasharray'] = u','.join([u'%f' % val for val in seq])
attrib[u'stroke-dashoffset'] = unicode(float(offset))
linewidth = gc.get_linewidth()
if linewidth:
attrib[u'stroke'] = rgb2hex(gc.get_rgb())
if linewidth != 1.0:
attrib[u'stroke-width'] = str(linewidth)
if gc.get_joinstyle() != 'round':
attrib[u'stroke-linejoin'] = gc.get_joinstyle()
if gc.get_capstyle() != 'projecting':
attrib[u'stroke-linecap'] = _capstyle_d[gc.get_capstyle()]
return attrib
def _get_style(self, gc, rgbFace):
return generate_css(self._get_style_dict(gc, rgbFace))
def _get_clip(self, gc):
cliprect = gc.get_clip_rectangle()
clippath, clippath_trans = gc.get_clip_path()
if clippath is not None:
clippath_trans = self._make_flip_transform(clippath_trans)
dictkey = (id(clippath), str(clippath_trans))
elif cliprect is not None:
x, y, w, h = cliprect.bounds
y = self.height-(y+h)
dictkey = (x, y, w, h)
else:
return None
clip = self._clipd.get(dictkey)
if clip is None:
oid = self._make_id(u'p', dictkey)
if clippath is not None:
self._clipd[dictkey] = ((clippath, clippath_trans), oid)
else:
self._clipd[dictkey] = (dictkey, oid)
else:
clip, oid = clip
return oid
def _write_clips(self):
if not len(self._clipd):
return
writer = self.writer
writer.start('defs')
for clip, oid in self._clipd.values():
writer.start('clipPath', id=oid)
if len(clip) == 2:
clippath, clippath_trans = clip
path_data = self._convert_path(clippath, clippath_trans, simplify=False)
writer.element(u'path', d=path_data)
else:
x, y, w, h = clip
writer.element(u'rect', x=unicode(x), y=unicode(y), width=unicode(w), height=unicode(h))
writer.end(u'clipPath')
writer.end(u'defs')
def _write_svgfonts(self):
if not rcParams['svg.fonttype'] == 'svgfont':
return
writer = self.writer
writer.start(u'defs')
for font_fname, chars in self._fonts.items():
font = FT2Font(font_fname)
font.set_size(72, 72)
sfnt = font.get_sfnt()
writer.start(u'font', id=sfnt[(1, 0, 0, 4)])
writer.element(
u'font-face',
attrib={
u'font-family': font.family_name,
u'font-style': font.style_name.lower(),
u'units-per-em': u'72',
u'bbox': u' '.join(unicode(x / 64.0) for x in font.bbox)})
for char in chars:
glyph = font.load_char(char, flags=LOAD_NO_HINTING)
verts, codes = font.get_path()
path = Path(verts, codes)
path_data = self._convert_path(path)
# name = font.get_glyph_name(char)
writer.element(
u'glyph',
d=path_data,
attrib={
# 'glyph-name': name,
u'unicode': unichr(char),
u'horiz-adv-x': unicode(glyph.linearHoriAdvance / 65536.0)})
writer.end(u'font')
writer.end(u'defs')
def open_group(self, s, gid=None):
"""
Open a grouping element with label *s*. If *gid* is given, use
*gid* as the id of the group.
"""
if gid:
self.writer.start('g', id=gid)
else:
self._groupd[s] = self._groupd.get(s, 0) + 1
self.writer.start(u'g', id=u"%s_%d" % (s, self._groupd[s]))
def close_group(self, s):
self.writer.end('g')
def option_image_nocomposite(self):
"""
if svg.image_noscale is True, compositing multiple images into one is prohibited
"""
return rcParams['svg.image_noscale']
def _convert_path(self, path, transform=None, clip=None, simplify=None):
if clip:
clip = (0.0, 0.0, self.width, self.height)
else:
clip = None
return _path.convert_to_svg(path, transform, clip, simplify, 6)
def draw_path(self, gc, path, transform, rgbFace=None):
trans_and_flip = self._make_flip_transform(transform)
clip = (rgbFace is None and gc.get_hatch_path() is None)
simplify = path.should_simplify and clip
path_data = self._convert_path(
path, trans_and_flip, clip=clip, simplify=simplify)
attrib = {}
attrib[u'style'] = self._get_style(gc, rgbFace)
clipid = self._get_clip(gc)
if clipid is not None:
attrib[u'clip-path'] = u'url(#%s)' % clipid
if gc.get_url() is not None:
self.writer.start(u'a', {u'xlink:href': gc.get_url()})
self.writer.element(u'path', d=path_data, attrib=attrib)
if gc.get_url() is not None:
self.writer.end(u'a')
def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None):
if not len(path.vertices):
return
writer = self.writer
path_data = self._convert_path(
marker_path,
marker_trans + Affine2D().scale(1.0, -1.0),
simplify=False)
style = self._get_style_dict(gc, rgbFace)
dictkey = (path_data, generate_css(style))
oid = self._markers.get(dictkey)
for key in style.keys():
if not key.startswith('stroke'):
del style[key]
style = generate_css(style)
if oid is None:
oid = self._make_id(u'm', dictkey)
writer.start(u'defs')
writer.element(u'path', id=oid, d=path_data, style=style)
writer.end(u'defs')
self._markers[dictkey] = oid
attrib = {}
clipid = self._get_clip(gc)
if clipid is not None:
attrib[u'clip-path'] = u'url(#%s)' % clipid
writer.start(u'g', attrib=attrib)
trans_and_flip = self._make_flip_transform(trans)
attrib = {u'xlink:href': u'#%s' % oid}
for vertices, code in path.iter_segments(trans_and_flip, simplify=False):
if len(vertices):
x, y = vertices[-2:]
attrib[u'x'] = unicode(x)
attrib[u'y'] = unicode(y)
attrib[u'style'] = self._get_style(gc, rgbFace)
writer.element(u'use', attrib=attrib)
writer.end('g')
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
writer = self.writer
path_codes = []
writer.start(u'defs')
for i, (path, transform) in enumerate(self._iter_collection_raw_paths(
master_transform, paths, all_transforms)):
transform = Affine2D(transform.get_matrix()).scale(1.0, -1.0)
d = self._convert_path(path, transform, simplify=False)
oid = u'C%x_%x_%s' % (self._path_collection_id, i,
self._make_id(u'', d))
writer.element(u'path', id=oid, d=d)
path_codes.append(oid)
writer.end(u'defs')
for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
gc, master_transform, all_transforms, path_codes, offsets,
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
antialiaseds, urls, offset_position):
clipid = self._get_clip(gc0)
url = gc0.get_url()
if url is not None:
writer.start(u'a', attrib={u'xlink:href': url})
if clipid is not None:
writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid})
attrib = {
u'xlink:href': u'#%s' % path_id,
u'x': unicode(xo),
u'y': unicode(self.height - yo),
u'style': self._get_style(gc0, rgbFace)
}
writer.element(u'use', attrib=attrib)
if clipid is not None:
writer.end(u'g')
if url is not None:
writer.end(u'a')
self._path_collection_id += 1
def draw_gouraud_triangle(self, gc, points, colors, trans):
# This uses a method described here:
#
# http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html
#
# that uses three overlapping linear gradients to simulate a
# Gouraud triangle. Each gradient goes from fully opaque in
# one corner to fully transparent along the opposite edge.
# The line between the stop points is perpendicular to the
# opposite edge. Underlying these three gradients is a solid
# triangle whose color is the average of all three points.
writer = self.writer
if not self._has_gouraud:
self._has_gouraud = True
writer.start(
u'filter',
id=u'colorAdd')
writer.element(
u'feComposite',
attrib={u'in': u'SourceGraphic'},
in2=u'BackgroundImage',
operator=u'arithmetic',
k2=u"1", k3=u"1")
writer.end(u'filter')
avg_color = np.sum(colors[:, :], axis=0) / 3.0
# Just skip fully-transparent triangles
if avg_color[-1] == 0.0:
return
trans_and_flip = self._make_flip_transform(trans)
tpoints = trans_and_flip.transform(points)
writer.start(u'defs')
for i in range(3):
x1, y1 = tpoints[i]
x2, y2 = tpoints[(i + 1) % 3]
x3, y3 = tpoints[(i + 2) % 3]
c = colors[i][:]
if x2 == x3:
xb = x2
yb = y1
elif y2 == y3:
xb = x1
yb = y2
else:
m1 = (y2 - y3) / (x2 - x3)
b1 = y2 - (m1 * x2)
m2 = -(1.0 / m1)
b2 = y1 - (m2 * x1)
xb = (-b1 + b2) / (m1 - m2)
yb = m2 * xb + b2
writer.start(
u'linearGradient',
id=u"GR%x_%d" % (self._n_gradients, i),
x1=unicode(x1), y1=unicode(y1), x2=unicode(xb), y2=unicode(yb))
writer.element(
u'stop',
offset=u'0',
style=generate_css({'stop-color': rgb2hex(c),
'stop-opacity': unicode(c[-1])}))
writer.element(
u'stop',
offset=u'1',
style=generate_css({u'stop-color': rgb2hex(c),
u'stop-opacity': u"0"}))
writer.end(u'linearGradient')
writer.element(
u'polygon',
id=u'GT%x' % self._n_gradients,
points=u" ".join([unicode(x) for x in x1,y1,x2,y2,x3,y3]))
writer.end('defs')
avg_color = np.sum(colors[:, :], axis=0) / 3.0
href = u'#GT%x' % self._n_gradients
writer.element(
u'use',
attrib={u'xlink:href': href,
u'fill': rgb2hex(avg_color),
u'fill-opacity': str(avg_color[-1])})
for i in range(3):
writer.element(
u'use',
attrib={u'xlink:href': href,
u'fill': u'url(#GR%x_%d)' % (self._n_gradients, i),
u'fill-opacity': u'1',
u'filter': u'url(#colorAdd)'})
self._n_gradients += 1
def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
transform):
attrib = {}
clipid = self._get_clip(gc)
if clipid is not None:
attrib[u'clip-path'] = u'url(#%s)' % clipid
self.writer.start(u'g', attrib=attrib)
transform = transform.frozen()
for tri, col in zip(triangles_array, colors_array):
self.draw_gouraud_triangle(gc, tri, col, transform)
self.writer.end(u'g')
def option_scale_image(self):
return True
def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None):
attrib = {}
clipid = self._get_clip(gc)
if clipid is not None:
# Can't apply clip-path directly to the image because the
# image has a transformation, which would also be applied
# to the clip-path
self.writer.start(u'g', attrib={u'clip-path': u'url(#%s)' % clipid})
trans = [1,0,0,1,0,0]
if rcParams['svg.image_noscale']:
trans = list(im.get_matrix())
trans[5] = -trans[5]
attrib[u'transform'] = generate_transform([(u'matrix', tuple(trans))])
assert trans[1] == 0
assert trans[2] == 0
numrows, numcols = im.get_size()
im.reset_matrix()
im.set_interpolation(0)
im.resize(numcols, numrows)
h,w = im.get_size_out()
oid = getattr(im, '_gid', None)
url = getattr(im, '_url', None)
if url is not None:
self.writer.start(u'a', attrib={u'xlink:href': url})
if rcParams['svg.image_inline']:
bytesio = io.BytesIO()
im.flipud_out()
rows, cols, buffer = im.as_rgba_str()
_png.write_png(buffer, cols, rows, bytesio)
im.flipud_out()
oid = oid or self._make_id('image', bytesio)
attrib['xlink:href'] = (
u"data:image/png;base64,\n" +
base64.b64encode(bytesio.getvalue()).decode('ascii'))
else:
self._imaged[self.basename] = self._imaged.get(self.basename,0) + 1
filename = u'%s.image%d.png'%(self.basename, self._imaged[self.basename])
verbose.report( 'Writing image file for inclusion: %s' % filename)
im.flipud_out()
rows, cols, buffer = im.as_rgba_str()
_png.write_png(buffer, cols, rows, filename)
im.flipud_out()
oid = oid or 'Im_' + self._make_id('image', filename)
attrib[u'xlink:href'] = filename
alpha = gc.get_alpha()
if alpha != 1.0:
attrib['opacity'] = str(alpha)
attrib['id'] = oid
if transform is None:
self.writer.element(
u'image',
x=unicode(x/trans[0]), y=unicode((self.height-y)/trans[3]-h),
width=unicode(w), height=unicode(h),
attrib=attrib)
else:
flipped = self._make_flip_transform(transform)
flipped = np.array(flipped.to_values())
y = y+dy
if dy > 0.0:
flipped[3] *= -1.0
y *= -1.0
attrib[u'transform'] = generate_transform(
[(u'matrix', flipped)])
self.writer.element(
u'image',
x=unicode(x), y=unicode(y),
width=unicode(dx), height=unicode(abs(dy)),
attrib=attrib)
if url is not None:
self.writer.end(u'a')
if clipid is not None:
self.writer.end(u'g')
def _adjust_char_id(self, char_id):
return char_id.replace(u"%20", u"_")
def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
"""
draw the text by converting them to paths using textpath module.
*prop*
font property
*s*
text to be converted
*usetex*
If True, use matplotlib usetex mode.
*ismath*
If True, use mathtext parser. If "TeX", use *usetex* mode.
"""
writer = self.writer
writer.comment(s)
glyph_map=self._glyph_map
text2path = self._text2path
color = rgb2hex(gc.get_rgb())
fontsize = prop.get_size_in_points()
style = {}
if color != '#000000':
style['fill'] = color
if gc.get_alpha() != 1.0:
style[u'opacity'] = unicode(gc.get_alpha())
if not ismath:
font = text2path._get_font(prop)
_glyphs = text2path.get_glyphs_with_font(
font, s, glyph_map=glyph_map, return_new_glyphs_only=True)
glyph_info, glyph_map_new, rects = _glyphs
y -= ((font.get_descent() / 64.0) *
(prop.get_size_in_points() / text2path.FONT_SCALE))
if glyph_map_new:
writer.start(u'defs')
for char_id, glyph_path in glyph_map_new.iteritems():
path = Path(*glyph_path)
path_data = self._convert_path(path, simplify=False)
writer.element(u'path', id=char_id, d=path_data)
writer.end(u'defs')
glyph_map.update(glyph_map_new)
attrib = {}
attrib[u'style'] = generate_css(style)
font_scale = fontsize / text2path.FONT_SCALE
attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,)),
(u'scale', (font_scale, -font_scale))])
writer.start(u'g', attrib=attrib)
for glyph_id, xposition, yposition, scale in glyph_info:
attrib={u'xlink:href': u'#%s' % glyph_id}
if xposition != 0.0:
attrib[u'x'] = str(xposition)
if yposition != 0.0:
attrib[u'y'] = str(yposition)
writer.element(
u'use',
attrib=attrib)
writer.end(u'g')
else:
if ismath == "TeX":
_glyphs = text2path.get_glyphs_tex(prop, s, glyph_map=glyph_map,
return_new_glyphs_only=True)
else:
_glyphs = text2path.get_glyphs_mathtext(prop, s, glyph_map=glyph_map,
return_new_glyphs_only=True)
glyph_info, glyph_map_new, rects = _glyphs
# we store the character glyphs w/o flipping. Instead, the
# coordinate will be flipped when this characters are
# used.
if glyph_map_new:
writer.start(u'defs')
for char_id, glyph_path in glyph_map_new.iteritems():
char_id = self._adjust_char_id(char_id)
# Some characters are blank
if not len(glyph_path[0]):
path_data = u""
else:
path = Path(*glyph_path)
path_data = self._convert_path(path, simplify=False)
writer.element(u'path', id=char_id, d=path_data)
writer.end(u'defs')
glyph_map.update(glyph_map_new)
attrib = {}
font_scale = fontsize / text2path.FONT_SCALE
attrib[u'style'] = generate_css(style)
attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,)),
(u'scale', (font_scale, -font_scale))])
writer.start(u'g', attrib=attrib)
for char_id, xposition, yposition, scale in glyph_info:
char_id = self._adjust_char_id(char_id)
writer.element(
u'use',
transform=generate_transform([
(u'translate', (xposition, yposition)),
(u'scale', (scale,)),
]),
attrib={u'xlink:href': u'#%s' % char_id})
for verts, codes in rects:
path = Path(verts, codes)
path_data = self._convert_path(path, simplify=False)
writer.element(u'path', d=path_data)
writer.end('g')
def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath):
writer = self.writer
color = rgb2hex(gc.get_rgb())
style = {}
if color != '#000000':
style[u'fill'] = color
if gc.get_alpha() != 1.0:
style[u'opacity'] = unicode(gc.get_alpha())
if not ismath:
font = self._get_font(prop)
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
y -= font.get_descent() / 64.0
fontsize = prop.get_size_in_points()
fontfamily = font.family_name
fontstyle = prop.get_style()
attrib = {}
# Must add "px" to workaround a Firefox bug
style[u'font-size'] = str(fontsize) + 'px'
style[u'font-family'] = str(fontfamily)
style[u'font-style'] = prop.get_style().lower()
attrib[u'style'] = generate_css(style)
attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,))])
writer.element(u'text', s, attrib=attrib)
if rcParams['svg.fonttype'] == 'svgfont':
fontset = self._fonts.setdefault(font.fname, set())
for c in s:
fontset.add(ord(c))
else:
writer.comment(s)
width, height, descent, svg_elements, used_characters = \
self.mathtext_parser.parse(s, 72, prop)
svg_glyphs = svg_elements.svg_glyphs
svg_rects = svg_elements.svg_rects
attrib = {}
attrib[u'style'] = generate_css(style)
attrib[u'transform'] = generate_transform([
(u'translate', (x, y)),
(u'rotate', (-angle,))])
# Apply attributes to 'g', not 'text', because we likely
# have some rectangles as well with the same style and
# transformation
writer.start(u'g', attrib=attrib)
writer.start(u'text')
# Sort the characters by font, and output one tspan for
# each
spans = {}
for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
style = generate_css({
u'font-size': unicode(fontsize) + 'px',
u'font-family': font.family_name,
u'font-style': font.style_name.lower()})
if thetext == 32:
thetext = 0xa0 # non-breaking space
spans.setdefault(style, []).append((new_x, -new_y, thetext))
if rcParams['svg.fonttype'] == 'svgfont':
for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs:
fontset = self._fonts.setdefault(font.fname, set())
fontset.add(thetext)
for style, chars in spans.items():
chars.sort()
same_y = True
if len(chars) > 1:
last_y = chars[0][1]
for i in xrange(1, len(chars)):
if chars[i][1] != last_y:
same_y = False
break
if same_y:
ys = unicode(chars[0][1])
else:
ys = ' '.join(unicode(c[1]) for c in chars)
attrib = {
u'style': style,
u'x': ' '.join(unicode(c[0]) for c in chars),
u'y': ys
}
writer.element(
u'tspan',
u''.join(unichr(c[2]) for c in chars),
attrib=attrib)
writer.end(u'text')
if len(svg_rects):
for x, y, width, height in svg_rects:
writer.element(
u'rect',
x=unicode(x), y=unicode(-y + height),
width=unicode(width), height=unicode(height)
)
writer.end(u'g')
def draw_tex(self, gc, x, y, s, prop, angle):
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")
def draw_text(self, gc, x, y, s, prop, angle, ismath):
clipid = self._get_clip(gc)
if clipid is not None:
# Cannot apply clip-path directly to the text, because
# is has a transformation
self.writer.start(
u'g', attrib={u'clip-path': u'url(#%s)' % clipid})
if rcParams['svg.fonttype'] == 'path':
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
else:
self._draw_text_as_text(gc, x, y, s, prop, angle, ismath)
if clipid is not None:
self.writer.end(u'g')
def flipy(self):
return True
def get_canvas_width_height(self):
return self.width, self.height
def get_text_width_height_descent(self, s, prop, ismath):
return self._text2path.get_text_width_height_descent(s, prop, ismath)
class FigureCanvasSVG(FigureCanvasBase):
filetypes = {'svg': 'Scalable Vector Graphics',
'svgz': 'Scalable Vector Graphics'}
def print_svg(self, filename, *args, **kwargs):
if is_string_like(filename):
fh_to_close = svgwriter = io.open(filename, 'w', encoding='utf-8')
elif is_writable_file_like(filename):
if not isinstance(filename, io.TextIOBase):
if sys.version_info[0] >= 3:
svgwriter = io.TextIOWrapper(filename, 'utf-8')
else:
svgwriter = codecs.getwriter('utf-8')(filename)
fh_to_close = None
else:
raise ValueError("filename must be a path or a file-like object")
return self._print_svg(filename, svgwriter, fh_to_close, **kwargs)
def print_svgz(self, filename, *args, **kwargs):
if is_string_like(filename):
fh_to_close = gzipwriter = gzip.GzipFile(filename, 'w')
svgwriter = io.TextIOWrapper(gzipwriter, 'utf-8')
elif is_writable_file_like(filename):
fh_to_close = gzipwriter = gzip.GzipFile(fileobj=filename, mode='w')
svgwriter = io.TextIOWrapper(gzipwriter, 'utf-8')
else:
raise ValueError("filename must be a path or a file-like object")
return self._print_svg(filename, svgwriter, fh_to_close)
def _print_svg(self, filename, svgwriter, fh_to_close=None, **kwargs):
try:
self.figure.set_dpi(72.0)
width, height = self.figure.get_size_inches()
w, h = width*72, height*72
if rcParams['svg.image_noscale']:
renderer = RendererSVG(w, h, svgwriter, filename)
else:
# setting mixed renderer dpi other than 72 results in
# incorrect size of the rasterized image. It seems that the
# svg internally uses fixed dpi of 72 and seems to cause
# the problem. I hope someone who knows the svg backends
# take a look at this problem. Meanwhile, the dpi
# parameter is ignored and image_dpi is fixed at 72. - JJL
#image_dpi = kwargs.pop("dpi", 72)
image_dpi = 72
_bbox_inches_restore = kwargs.pop("bbox_inches_restore", None)
renderer = MixedModeRenderer(self.figure,
width, height, image_dpi, RendererSVG(w, h, svgwriter, filename),
bbox_inches_restore=_bbox_inches_restore)
self.figure.draw(renderer)
renderer.finalize()
finally:
if fh_to_close is not None:
svgwriter.close()
def get_default_filetype(self):
return 'svg'
class FigureManagerSVG(FigureManagerBase):
pass
FigureManager = FigureManagerSVG
def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
return new_figure_manager_given_figure(num, thisFig)
def new_figure_manager_given_figure(num, figure):
"""
Create a new figure manager instance for the given figure.
"""
canvas = FigureCanvasSVG(figure)
manager = FigureManagerSVG(canvas, num)
return manager
svgProlog = u"""\
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Created with matplotlib (http://matplotlib.org/) -->
"""
Jump to Line
Something went wrong with that request. Please try again.