svg exporter updates

This commit is contained in:
Luke Campagnola 2012-12-25 22:20:31 -05:00
parent af59296231
commit b0e0781624
4 changed files with 225 additions and 95 deletions

View File

@ -50,7 +50,7 @@ CONFIG_OPTIONS = {
def setConfigOption(opt, value): def setConfigOption(opt, value):
CONFIG_OPTIONS[opt] = value CONFIG_OPTIONS[opt] = value
def setConfigOptions(opts): def setConfigOptions(**opts):
CONFIG_OPTIONS.update(opts) CONFIG_OPTIONS.update(opts)
def getConfigOption(opt): def getConfigOption(opt):

View File

@ -1,6 +1,7 @@
from .Exporter import Exporter from .Exporter import Exporter
from pyqtgraph.parametertree import Parameter from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import pyqtgraph as pg
import re import re
import xml.dom.minidom as xml import xml.dom.minidom as xml
@ -73,15 +74,10 @@ class SVGExporter(Exporter):
##print "new line:", data[i] ##print "new line:", data[i]
#open(fileName, 'w').write(''.join(data)) #open(fileName, 'w').write(''.join(data))
node = self.generateItemSvg(self.item) ## Qt's SVG generator is not complete. (notably, it lacks clipping)
xml = """\ ## Instead, we will use Qt to generate SVG for each item independently,
<?xml version="1.0" encoding="UTF-8" standalone="no"?> ## then manually reconstruct the entire document.
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny"> xml = generateSvg(self.item)
<title>pyqtgraph SVG export</title>
<desc>Generated with Qt and pyqtgraph</desc>
<defs>
</defs>
""" + node.toprettyxml(indent=' ') + "\n</svg>\n"
if toBytes: if toBytes:
return bytes(xml) return bytes(xml)
@ -89,90 +85,205 @@ class SVGExporter(Exporter):
with open(fileName, 'w') as fh: with open(fileName, 'w') as fh:
fh.write(xml) fh.write(xml)
def generateItemSvg(self, item): xmlHeader = """\
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
<title>pyqtgraph SVG export</title>
<desc>Generated with Qt and pyqtgraph</desc>
<defs>
</defs>
"""
def generateSvg(item):
global xmlHeader
try:
node = _generateItemSvg(item)
finally:
## reset export mode for all items in the tree
if isinstance(item, QtGui.QGraphicsScene): if isinstance(item, QtGui.QGraphicsScene):
xmlStr = "<g></g>" items = item.items()
childs = [i for i in item.items() if i.parentItem() is None]
else: else:
tr = QtGui.QTransform() items = [item]
tr.translate(item.pos().x(), item.pos().y()) for i in items:
tr = tr * item.transform() items.extend(i.childItems())
if not item.isVisible() or int(item.flags() & item.ItemHasNoContents) > 0: for i in items:
m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) if hasattr(i, 'setExportMode'):
#print item, m i.setExportMode(False)
xmlStr = '<g transform="matrix(%f,%f,%f,%f,%f,%f)"></g>' % m
else:
arr = QtCore.QByteArray()
buf = QtCore.QBuffer(arr)
svg = QtSvg.QSvgGenerator()
svg.setOutputDevice(buf)
dpi = QtGui.QDesktopWidget().physicalDpiX()
### not really sure why this works, but it seems to be important:
#self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.))
svg.setResolution(dpi)
p = QtGui.QPainter() cleanXml(node)
p.begin(svg)
if hasattr(item, 'setExportMode'):
item.setExportMode(True, {'painter': p})
try:
#tr = QtGui.QTransform()
#tr.translate(item.pos().x(), item.pos().y())
#p.setTransform(tr * item.transform())
p.setTransform(tr)
item.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
finally:
p.end()
if hasattr(item, 'setExportMode'):
item.setExportMode(False)
xmlStr = str(arr) return xmlHeader + node.toprettyxml(indent=' ') + "\n</svg>\n"
childs = item.childItems()
def _generateItemSvg(item, nodes=None, root=None):
## This function is intended to work around some issues with Qt's SVG generator
## and SVG in general.
## 1) Qt SVG does not implement clipping paths. This is absurd.
## The solution is to let Qt generate SVG for each item independently,
## then glue them together manually with clipping.
## 2) There seems to be wide disagreement over whether path strokes
## should be scaled anisotropically.
## see: http://web.mit.edu/jonas/www/anisotropy/
## Given that both inkscape and illustrator seem to prefer isotropic
## scaling, we will optimize for those cases.
## 3) Qt generates paths using non-scaling-stroke from SVG 1.2, but
## inkscape only supports 1.1.
if nodes is None: ## nodes maps all node IDs to their XML element.
## this allows us to ensure all elements receive unique names.
nodes = {}
if root is None:
root = item
## Skip hidden items
if hasattr(item, 'isVisible') and not item.isVisible():
return None
## If this item defines its own SVG generator, use that.
if hasattr(item, 'generateSvg'):
return item.generateSvg(nodes)
## Generate SVG text for just this item (exclude its children; we'll handle them later)
tr = QtGui.QTransform()
if isinstance(item, QtGui.QGraphicsScene):
xmlStr = "<g>\n</g>\n"
childs = [i for i in item.items() if i.parentItem() is None]
doc = xml.parseString(xmlStr) doc = xml.parseString(xmlStr)
else:
childs = item.childItems()
tr.translate(item.pos().x(), item.pos().y())
tr = tr * item.transform()
#if not item.isVisible() or int(item.flags() & item.ItemHasNoContents) > 0:
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
##print item, m
#xmlStr = '<g><g transform="matrix(%f,%f,%f,%f,%f,%f)"></g></g>' % m # note: outer group is needed to separate clipping from transform
#doc = xml.parseString(xmlStr)
#else:
arr = QtCore.QByteArray()
buf = QtCore.QBuffer(arr)
svg = QtSvg.QSvgGenerator()
svg.setOutputDevice(buf)
dpi = QtGui.QDesktopWidget().physicalDpiX()
### not really sure why this works, but it seems to be important:
#self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.))
svg.setResolution(dpi)
p = QtGui.QPainter()
p.begin(svg)
if hasattr(item, 'setExportMode'):
item.setExportMode(True, {'painter': p})
try: try:
groups = doc.getElementsByTagName('g') #p.setTransform(tr)
if len(groups) == 1: item.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
g1 = g2 = groups[0] finally:
else: p.end()
g1,g2 = groups[:2] ## Can't do this here--we need to wait until all children have painted as well.
except: ## this is taken care of in generateSvg instead.
print doc.toxml() #if hasattr(item, 'setExportMode'):
raise #item.setExportMode(False)
g1.setAttribute('id', item.__class__.__name__)
## Check for item visibility xmlStr = str(arr)
visible = True doc = xml.parseString(xmlStr)
if not isinstance(item, QtGui.QGraphicsScene):
parent = item
while visible and parent is not None:
visible = parent.isVisible()
parent = parent.parentItem()
if not visible: try:
style = g1.getAttribute('style').strip() ## Get top-level group for this item
if len(style)>0 and not style.endswith(';'): g1 = doc.getElementsByTagName('g')[0]
style += ';' ## get list of sub-groups
style += 'display:none;' g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
g1.setAttribute('style', style) except:
print doc.toxml()
raise
childs.sort(key=lambda c: c.zValue()) ## make sure g1 has the transformation matrix
for ch in childs: m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
cg = self.generateItemSvg(ch) g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
g2.appendChild(cg)
return g1 #print "=================",item,"====================="
#print g1.toprettyxml(indent=" ", newl='')
## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1)
### To check: ## So we need to correct anything attempting to use this.
### do all items really generate this double-group structure? correctStroke(g1, item, root)
### are both groups necessary?
### How do we implement clipping? (can we clip to an object that is visible?) ## decide on a name for this item
baseName = item.__class__.__name__
i = 1
while True:
name = baseName + "_%d" % i
if name not in nodes:
break
i += 1
nodes[name] = g1
g1.setAttribute('id', name)
## If this item clips its children, we need to take car of that.
childGroup = g1 ## add children directly to this node unless we are clipping
if not isinstance(item, QtGui.QGraphicsScene):
## See if this item clips its children
if int(item.flags() & item.ItemClipsChildrenToShape) > 0:
## Generate svg for just the path
path = QtGui.QGraphicsPathItem(item.shape())
pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
## and for the clipPath element
clip = name + '_clip'
clipNode = g1.ownerDocument.createElement('clipPath')
clipNode.setAttribute('id', clip)
clipNode.appendChild(pathNode)
g1.appendChild(clipNode)
childGroup = g1.ownerDocument.createElement('g')
childGroup.setAttribute('clip-path', 'url(#%s)' % clip)
g1.appendChild(childGroup)
## Add all child items as sub-elements.
childs.sort(key=lambda c: c.zValue())
for ch in childs:
cg = _generateItemSvg(ch, nodes, root)
if cg is None:
continue
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
return g1
def correctStroke(node, item, root, width=1):
#print "==============", item, node
if node.hasAttribute('stroke-width'):
width = float(node.getAttribute('stroke-width'))
if node.getAttribute('vector-effect') == 'non-scaling-stroke':
node.removeAttribute('vector-effect')
if isinstance(root, QtGui.QGraphicsScene):
w = item.mapFromScene(pg.Point(width,0))
o = item.mapFromScene(pg.Point(0,0))
else:
w = item.mapFromItem(root, pg.Point(width,0))
o = item.mapFromItem(root, pg.Point(0,0))
w = w-o
#print " ", w, o, w-o
w = (w.x()**2 + w.y()**2) ** 0.5
#print " ", w
node.setAttribute('stroke-width', str(w))
for ch in node.childNodes:
if isinstance(ch, xml.Element):
correctStroke(ch, item, root, width)
def cleanXml(node):
## remove extraneous text; let the xml library do the formatting.
hasElement = False
nonElement = []
for ch in node.childNodes:
if isinstance(ch, xml.Element):
hasElement = True
cleanXml(ch)
else:
nonElement.append(ch)
if hasElement:
for ch in nonElement:
node.removeChild(ch)

View File

@ -21,7 +21,7 @@ class RemoteGraphicsView(QtGui.QWidget):
QtGui.QWidget.__init__(self) QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess() self._proc = mp.QtProcess()
self.pg = self._proc._import('pyqtgraph') self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(self.pg.CONFIG_OPTIONS) self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
self._view = rpgRemote.Renderer(*args, **kwds) self._view = rpgRemote.Renderer(*args, **kwds)
self._view._setProxyOptions(deferGetattr=True) self._view._setProxyOptions(deferGetattr=True)

View File

@ -7,20 +7,39 @@ app = pg.mkQApp()
class SVGTest(test.TestCase): class SVGTest(test.TestCase):
def test_plotscene(self): def test_plotscene(self):
p = pg.plot([1,5,2,3,4,6,1,2,4,2,3,5,3]) pg.setConfigOption('foreground', (0,0,0))
p.setXRange(0,5) w = pg.GraphicsWindow()
ex = pg.exporters.SVGExporter.SVGExporter(p.scene()) w.show()
p1 = w.addPlot()
p2 = w.addPlot()
p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'})
p1.setXRange(0,5)
p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3})
app.processEvents()
app.processEvents()
ex = pg.exporters.SVGExporter.SVGExporter(w.scene())
ex.export(fileName='test.svg') ex.export(fileName='test.svg')
#def test_simple(self): #def test_simple(self):
#rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
##rect.rotate(30) #rect.translate(50, 50)
#rect.rotate(30)
#grp = pg.ItemGroup() #grp = pg.ItemGroup()
#grp.setParentItem(rect) #grp.setParentItem(rect)
#grp.translate(200,0) #grp.translate(200,0)
#grp.rotate(30) ##grp.rotate(30)
#el = pg.QtGui.QGraphicsEllipseItem(10, 0, 100, 50)
#el.setParentItem(grp) #rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25)
#rect2.setFlag(rect2.ItemClipsChildrenToShape)
#rect2.setParentItem(grp)
#rect2.setPos(0,25)
#rect2.rotate(30)
#el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50)
#el.translate(10,-5)
#el.scale(0.5,2)
#el.setParentItem(rect2)
#ex = pg.exporters.SVGExporter.SVGExporter(rect) #ex = pg.exporters.SVGExporter.SVGExporter(rect)
#ex.export(fileName='test.svg') #ex.export(fileName='test.svg')