Hi, I wanted to be able to use Inkscape to fiddle with the
> details of my plot before exporting it to postscript, so I
> started writing an SVG backend. What I have so far is
> attached. Note that it's based on matplotlib-0.53.1, so it
> won't work with 0.54 yet. So far it just draws lines,
> rectangles, polygons, and text (only in helvetica).
> Before I go further, I wanted to make sure you or someone
> else is not already working on this. If not, I'll port it
> to use font-caching, the new transform stuff and figure out
> how to draw arcs and polygon collections, etc.
This is absolutely great news! Glad to hear you made it so far on
your own. I CCd the devel list, which you may want to join, in case
anyone else has any advice for you. As far as I know, noone else is
working on this.
Here are a few thoughts for you when porting to 0.54.
All the text layout has been moved to the text.Text front end. So you
no longer need to worry about horizontal alignment and the like,
compute_offsets is no longer needed, etc.. You also do not need to
provide the window extent of text anymore. Just provide
get_text_width_height(self, s, prop, ismath)
where s is a string and prop is a FontProperties instance. This
returns the width and height of the *unrotated* string. In the front
end I use this to compute the width and height of the rotated string,
compute the alignment offsets, and pass the x,y location to you to
draw the rotated string in draw_text. Instead of working with Text
instances, as in 0.53, you now work with strings and font properties.
The other new Renderer method is
get_canvas_width_height
As for polygon collections, you can implement this if you want, but
the base class provides an implementation that is reasonably fast
(approx 5x faster than the prior method of using separate polygon
instances). This may be good enough, since my guess is for most cases
you only demand the highest performance for interactive use, eg with
one of the GUI backends. So the *Agg backends implement this in
extension code.
As for draw_arc, currently this is only used for drawing circles, so
if you implemented draw_circle there is would suffice.
> The fact that I, a python novice, could get so far in a day
> attests to the awesomeness of matplotlib. Thanks!
A day... Amazing. Maybe we'll have you write the PDF backend next
JDH
from __future__ import division
from cStringIO import StringIO
from matplotlib.afm import AFM
from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\
FigureManagerBase, FigureCanvasBase
from matplotlib.cbook import iterable, is_string_like, flatten, enumerate,\
get_recursive_filelist, True, False
from matplotlib.figure import Figure
from matplotlib.font_manager import fontManager
from matplotlib.ft2font import FT2Font
from matplotlib._matlab_helpers import Gcf
from matplotlib.text import Text
from matplotlib.transforms import Bound1D, Bound2D, Transform
from matplotlib import rcParams
from matplotlib.numerix import fromstring, UInt8, Float32
import binascii
import sys,os
def error_msg_svg(msg, *args):
print >>sys.stderr, 'Error:', msg
sys.exit()
def _nums_to_str(seq, fmt='%1.3f'):
return ' '.join([_int_or_float(val, fmt) for val in seq])
def draw_if_interactive():
pass
def show():
"""
Show all the figures and enter the gtk mainloop
This should be the last line of your script
"""
for manager in Gcf.get_all_fig_managers():
manager.figure.realize()
def new_figure_manager(num, *args):
thisFig = Figure(*args)
canvas = FigureCanvasSVG(thisFig)
manager = FigureManagerSVG(canvas, num)
return manager
def _rgb_to_hex(rgb):
rgbhex='#'
for c in rgb:
h=hex(int(c*255))[2:]
if len(h) < 2:
h='0'+ h
rgbhex += h
return rgbhex
class RendererSVG(RendererBase):
def __init__(self, svgwriter,width,height):
self._svgwriter = svgwriter
self.width=width
self.height=height
def flipy(self):
'return true if y small numbers are top for renderer'
return False
def draw_rawsvg(self, svg):
self._svgwriter.write(svg)
def compute_text_offsets(self, t):
"""
Return the (x,y) offsets to adjust for the alignment
specifications
"""
prop = t.get_font_properties()
font = AFM(file(fontManager.findfont(prop, fontext='afm')))
text = t.get_text()
l,b,w,h = font.get_str_bbox(text)
fontsize = prop.get_size_in_points()
w *= 0.001*fontsize
h *= 0.001*fontsize
halign = t.get_horizontalalignment()
valign = t.get_verticalalignment()
if t.get_rotation()=='vertical':
w, h = h, w
if halign=='center': offsetx = w/2
elif halign=='right': offsetx = 0
else: offsetx = w
if valign=='center': offsety = h/2
elif valign=='top': offsety = h
else: offsety = 0
else:
if halign=='center': offsetx = -w/2
elif halign=='right': offsetx = -w
else: offsetx = 0
if valign=='center': offsety = h/2
elif valign=='top': offsety = h
else: offsety = 0
return (offsetx, offsety)
def draw_arc(self, gc, rgbFace, x, y, width, height, angle1, angle2):
pass
def draw_line(self, gc, x1, y1, x2, y2):
"""
Draw a single line from x1,y1 to x2,y2
"""
type = '<path '
details = ' d=\"M %f,%f L %f,%f\" ' % (x1,self.height-y1,x2,self.height-y2)
self._draw_svg(type, details, gc, None)
def draw_lines(self, gc, x, y):
"""
x and y are equal length arrays, draw lines connecting each
point in x, y
"""
if len(x)==0: return
if len(x)!=len(y): error_msg_svg('x and y must be the same length')
type = '<path '
details =' d=\"M %f,%f ' % (x[0], self.height-y[0])
for tup in zip(x[1:], self.height-y[1:]):
details += 'L %f,%f ' % tup
details += '\" '
self._draw_svg(type, details, gc, None)
def draw_rectangle(self, gc, rgbFace, x, y, width, height):
rgbhex='fill:#'
for c in rgbFace:
rgbhex += hex(int(c*255))[2:]
type = '<rect '
details = """
width=\"%f\"
height=\"%f\"
x=\"%f\"
y=\"%f\" """ % (width, height, x, self.height-y-height)
self._draw_svg(type, details, gc, rgbFace)
def draw_polygon(self, gc, rgbFace, points):
type = '<polygon '
details = 'points =\"'
for point in points:
details += '%f,%f ' % (point[0],self.height-point[1])
details += '\"'
self._draw_svg(type, details, gc, rgbFace)
def draw_text(self, gc, x, y, t):
"""
draw a Text instance
"""
prop = t.get_font_properties()
font = AFM(file(fontManager.findfont(prop, fontext='afm')))
text = t.get_text()
l,b,w,h = font.get_str_bbox(text)
if text=='': return
ox, oy = self.compute_text_offsets(t)
if t.get_rotation()=='vertical':
x = x+ox
y = self.height - y+oy-0.001*h
else:
x = x+ox
y = self.height - y+oy
thetext = '%s' % text
fontname = font.get_fontname()
fontsize = prop.get_size_in_points()
if t.get_rotation()=='vertical':
rotate = '90 rotate'
else:
rotate = ''
svg = '<text '
svg += """\
x=\"%f\"
y=\"%f\"
style=\"font-size:%f;stroke-width:1.0000000pt;font-family:helvetica;\" >
""" % (x,y,float(fontsize))
svg += thetext+' </text>'
self.draw_rawsvg(svg)
def get_svg(self):
return self._svgwriter.getvalue()
def finish(self):
self._svgwriter.write('</svg>')
def new_gc(self):
return GraphicsContextSVG()
def get_text_extent(self, t):
x, y = t.get_xy_display()
prop = t.get_font_properties()
font = AFM(file(fontManager.findfont(prop, fontext='afm')))
text = t.get_text()
l,b,w,h = font.get_str_bbox(text)
fontsize = prop.get_size_in_points()
l *= 0.001*fontsize
b *= 0.001*fontsize
w *= 0.001*fontsize
h *= 0.001*fontsize
ox, oy = self.compute_text_offsets(t)
left = x+ox+l
bottom = y-oy+b
if t.get_rotation()=='vertical':
w,h = h,w
return Bound2D(left, bottom, w, h)
def _draw_svg(self, type, details, gc, rgbFace):
svg=type
if rgbFace is not None:
rgbhex='fill:#'
for c in rgbFace:
rgbhex += hex(int(c*255))[2:]
rgbhex += ';'
else:
rgbhex='fill:none;'
style = self._get_gc_props_svg(gc)
svg+=style+rgbhex+ ' \"\n'
svg += details
svg += ' />\n'
self._svgwriter.write(svg)
def _get_gc_props_svg(self, gc):
color='stroke:'+_rgb_to_hex(gc.get_rgb())+';'
linewidth = 'stroke-width:'+repr(gc.get_linewidth())+'pt;'
join = 'stroke-linejoin:'+gc.get_joinstyle()+';'
cap = 'stroke-linecap:'+gc.get_capstyle()+';'
offset, seq = gc.get_dashes()
if seq is not None:
dashes = 'stroke-dasharray:'
for s in seq:
dashes += '%d ' % s
dashes += ';'
dashes += 'stroke-dashoffset:%f;' % offset
else:
dashes = ''
style = 'style=\"'+color+linewidth+join+cap+dashes
return style
class GraphicsContextSVG(GraphicsContextBase):
def set_linestyle(self, style):
GraphicsContextBase.set_linestyle(self, style)
offset, dashes = self._dashd[style]
self.set_dashes(offset, dashes)
class FigureCanvasSVG(FigureCanvasBase):
def draw(self):
pass
def print_figure(self, filename, dpi=100,
facecolor='w', edgecolor='w',
orientation='portrait'):
basename, ext = os.path.splitext(filename)
if not len(ext): filename += '.svg'
self._svgwriter = StringIO()
renderer = RendererSVG(self._svgwriter,self.figure.figsize[0]*dpi,self.figure.figsize[1]*dpi)
print dpi
print (self.figure.figsize[0]*dpi,
self.figure.figsize[1]*dpi)
self._svgwriter.write(_svgProlog % (renderer.width,renderer.height))
self.figure.draw(renderer)
renderer.finish()
try: fh = file(filename, 'w')
except IOError:
error_msg_svg('Could not open %s for writing' % filename)
return
print >>fh, renderer.get_svg()
class FigureManagerSVG(FigureManagerBase):
pass
FigureManager = FigureManagerSVG
error_msg = error_msg_svg
_svgProlog = """<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>
<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"
\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\\">
<!-- Created with matplotlib (http://matplotlib.sourceforge.net/) -->
<svg
xmlns=\"http://www.w3.org/2000/svg\\"
xmlns:xlink=\"http://www.w3.org/1999/xlink\\"
version=\"1.0\"
x=\"0.0000000\"
y=\"0.0000000\"
width=\"%f\"
height=\"%f\"
id=\"svg1\">
"""