from __future__ import division
import os, codecs, base64, tempfile
from matplotlib import verbose, __version__, rcParams
from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\
FigureManagerBase, FigureCanvasBase
from matplotlib.colors import rgb2hex
from matplotlib.figure import Figure
from matplotlib.font_manager import fontManager
from matplotlib.ft2font import FT2Font
from matplotlib.mathtext import math_parse_s_ft2font_svg
backend_version = __version__
def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args)
canvas = FigureCanvasSVG(thisFig)
manager = FigureManagerSVG(canvas, num)
return manager
_fontd = {}
_capstyle_d = {'projecting' : 'square', 'butt' : 'butt', 'round': 'round',}
class RendererSVG(RendererBase):
def __init__(self, width, height, svgwriter, basename=None):
self.width=width
self.height=height
self._svgwriter = svgwriter
self._groupd = {}
if not rcParams['svg.image_inline']:
assert basename is not None
self.basename = basename
self._imaged = {}
self._clipd = {}
svgwriter.write(svgProlog%(width,height,width,height))
def _draw_svg_element(self, element, details, gc, rgbFace):
cliprect, clipid = self._get_gc_clip_svg(gc)
if clipid is None:
clippath = ''
else:
clippath = 'clip-path="url(#%s)"' % clipid
self._svgwriter.write ('%s<%s %s %s %s/>\n' % (
cliprect,
element, self._get_style(gc, rgbFace), clippath, details))
def _get_font(self, prop):
key = hash(prop)
font = _fontd.get(key)
if font is None:
fname = fontManager.findfont(prop)
font = FT2Font(str(fname))
_fontd[key] = font
font.clear()
size = prop.get_size_in_points()
font.set_size(size, 72.0)
return font
def _get_style(self, gc, rgbFace):
"""
return the style string.
style is generated from the GraphicsContext, rgbFace and clippath
"""
if rgbFace is None:
fill = 'none'
else:
fill = rgb2hex(rgbFace)
offset, seq = gc.get_dashes()
if seq is None:
dashes = ''
else:
dashes = 'stroke-dasharray: %s; stroke-dashoffset: %f;' % (
' '.join(['%f'%val for val in seq]), offset)
linewidth = gc.get_linewidth()
if linewidth:
return 'style="fill: %s; stroke: %s; stroke-width: %f; ' \
'stroke-linejoin: %s; stroke-linecap: %s; %s opacity: %f"' % (
fill,
rgb2hex(gc.get_rgb()),
linewidth,
gc.get_joinstyle(),
_capstyle_d[gc.get_capstyle()],
dashes,
gc.get_alpha(),
)
else:
return 'style="fill: %s; opacity: %f"' % (\
fill,
gc.get_alpha(),
)
def _get_gc_clip_svg(self, gc):
cliprect = gc.get_clip_rectangle()
if cliprect is None:
return '', None
else:
# See if we've already seen this clip rectangle
key = hash(cliprect)
if self._clipd.get(key) is None: # If not, store a new clipPath
self._clipd[key] = cliprect
x, y, w, h = cliprect
y = self.height-(y+h)
box = """\
""" % locals()
return box, key
else:
# return id of previously defined clipPath
return '', key
def open_group(self, s):
self._groupd[s] = self._groupd.get(s,0) + 1
self._svgwriter.write('\n' % (s, self._groupd[s]))
def close_group(self, s):
self._svgwriter.write('\n')
def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2, rotation):
"""
Ignores angles for now
"""
details = 'cx="%f" cy="%f" rx="%f" ry="%f" transform="rotate(%f %f %f)"' % \
(x, self.height-y, width/2.0, height/2.0, -rotation, x, self.height-y)
self._draw_svg_element('ellipse', details, gc, rgbFace)
def option_image_nocomposite(self):
"""
if svg.image_noscale is True, compositing multiple images into one is prohibited
"""
return rcParams['svg.image_noscale']
def draw_image(self, x, y, im, bbox):
trans = [1,0,0,1,0,0]
transstr = ''
if rcParams['svg.image_noscale']:
trans = list(im.get_matrix())
if im.get_interpolation() != 0:
trans[4] += trans[0]
trans[5] += trans[3]
trans[5] = -trans[5]
transstr = 'transform="matrix(%f %f %f %f %f %f)" '%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()
if rcParams['svg.image_inline']:
filename = os.path.join (tempfile.gettempdir(),
tempfile.gettempprefix() + '.png'
)
verbose.report ('Writing temporary image file for inlining: %s' % filename)
# im.write_png() accepts a filename, not file object, would be
# good to avoid using files and write to mem with StringIO
# JDH: it *would* be good, but I don't know how to do this
# since libpng seems to want a FILE* and StringIO doesn't seem
# to provide one. I suspect there is a way, but I don't know
# it
im.flipud_out()
im.write_png(filename)
im.flipud_out()
imfile = file (filename, 'r')
image64 = base64.encodestring (imfile.read())
imfile.close()
os.remove(filename)
hrefstr = 'data:image/png;base64,\n' + image64
else:
self._imaged[self.basename] = self._imaged.get(self.basename,0) + 1
filename = '%s.image%d.png'%(self.basename, self._imaged[self.basename])
verbose.report( 'Writing image file for inclusion: %s' % filename)
im.flipud_out()
im.write_png(filename)
im.flipud_out()
hrefstr = filename
self._svgwriter.write (
'\n'%(x/trans[0], (self.height-y)/trans[3]-h, w, h, hrefstr, transstr)
)
def draw_line(self, gc, x1, y1, x2, y2):
details = 'd="M %f,%f L %f,%f"' % (x1, self.height-y1,
x2, self.height-y2)
self._draw_svg_element('path', details, gc, None)
def draw_lines(self, gc, x, y, transform=None):
if len(x)==0: return
if len(x)!=len(y):
raise ValueError('x and y must be the same length')
y = self.height - y
details = ['d="M %f,%f' % (x[0], y[0])]
xys = zip(x[1:], y[1:])
details.extend(['L %f,%f' % tup for tup in xys])
details.append('"')
details = ' '.join(details)
self._draw_svg_element('path', details, gc, None)
def draw_point(self, gc, x, y):
# result seems to have a hole in it...
self.draw_arc(gc, gc.get_rgb(), x, y, 1, 0, 0, 0, 0)
def draw_polygon(self, gc, rgbFace, points):
details = 'points = "%s"' % ' '.join(['%f,%f'%(x,self.height-y)
for x, y in points])
self._draw_svg_element('polygon', details, gc, rgbFace)
def draw_rectangle(self, gc, rgbFace, x, y, width, height):
details = 'width="%f" height="%f" x="%f" y="%f"' % (width, height, x,
self.height-y-height)
self._draw_svg_element('rect', details, gc, rgbFace)
def draw_text(self, gc, x, y, s, prop, angle, ismath):
if ismath:
self._draw_mathtext(gc, x, y, s, prop, angle)
return
font = self._get_font(prop)
thetext = '%s' % s
fontfamily=font.family_name
fontstyle=font.style_name
fontsize = prop.get_size_in_points()
color = rgb2hex(gc.get_rgb())
style = 'font-size: %f; font-family: %s; font-style: %s; fill: %s;'%(fontsize, fontfamily,fontstyle, color)
if angle!=0:
transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) # Inkscape doesn't support rotate(angle x y)
else: transform = ''
svg = """\
%(thetext)s
""" % locals()
self._svgwriter.write (svg)
def _draw_mathtext(self, gc, x, y, s, prop, angle):
"""
Draw math text using matplotlib.mathtext
"""
fontsize = prop.get_size_in_points()
width, height, svg_elements = math_parse_s_ft2font_svg(s,
72, fontsize)
svg_glyphs = svg_elements.svg_glyphs
svg_lines = svg_elements.svg_lines
color = rgb2hex(gc.get_rgb())
svg = ""
self.open_group("mathtext")
for fontname, fontsize, thetext, ox, oy, metrics in svg_glyphs:
style = 'font-size: %f; font-family: %s; fill: %s;'%(fontsize,
fontname, color)
if angle!=0:
transform = 'transform="translate(%f,%f) rotate(%1.1f) translate(%f,%f)"' % (x,y,-angle,-x,-y) # Inkscape doesn't support rotate(angle x y)
else: transform = ''
#newx, newy = x+ox, y-oy
newx, newy = x+ox, y+oy-height
svg += """\
%(thetext)s
""" % locals()
self._svgwriter.write (svg)
rgbFace = gc.get_rgb()
for xmin, ymin, xmax, ymax in svg_lines:
newx, newy = x + xmin, y + height - ymax#-(ymax-ymin)/2#-height
self.draw_rectangle(gc, rgbFace, newx, newy, xmax-xmin, ymax-ymin)
#~ self.draw_line(gc, x + xmin, y + ymin,# - height,
#~ x + xmax, y + ymax)# - height)
self.close_group("mathtext")
def finish(self):
self._svgwriter.write('\n')
def flipy(self):
return True
def get_canvas_width_height(self):
return self.width, self.height
def get_text_width_height(self, s, prop, ismath):
if ismath:
width, height, trash = math_parse_s_ft2font_svg(
s, 72, prop.get_size_in_points())
return width, height
font = self._get_font(prop)
font.set_text(s, 0.0)
w, h = font.get_width_height()
w /= 64.0 # convert from subpixels
h /= 64.0
return w, h
class FigureCanvasSVG(FigureCanvasBase):
def print_figure(self, filename, dpi, facecolor='w', edgecolor='w',
orientation='portrait', **kwargs):
# save figure settings
origDPI = self.figure.dpi.get()
origfacecolor = self.figure.get_facecolor()
origedgecolor = self.figure.get_edgecolor()
self.figure.dpi.set(72)
self.figure.set_facecolor(facecolor)
self.figure.set_edgecolor(edgecolor)
width, height = self.figure.get_size_inches()
w, h = width*72, height*72
basename, ext = os.path.splitext(filename)
if not len(ext): filename += '.svg'
svgwriter = codecs.open( filename, 'w', 'utf-8' )
renderer = RendererSVG(w, h, svgwriter, basename)
self.figure.draw(renderer)
renderer.finish()
# restore figure settings
self.figure.dpi.set(origDPI)
self.figure.set_facecolor(origfacecolor)
self.figure.set_edgecolor(origedgecolor)
svgwriter.close()
class FigureManagerSVG(FigureManagerBase):
pass
FigureManager = FigureManagerSVG
svgProlog = """\