svg exporter updates

This commit is contained in:
Luke Campagnola 2012-12-25 22:20:31 -05:00
parent c07a92efbe
commit f7f76101ca
3 changed files with 199 additions and 88 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,19 +85,84 @@ 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:
items = [item]
for i in items:
items.extend(i.childItems())
for i in items:
if hasattr(i, 'setExportMode'):
i.setExportMode(False)
cleanXml(node)
return xmlHeader + node.toprettyxml(indent=' ') + "\n</svg>\n"
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() 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)
else:
childs = item.childItems()
tr.translate(item.pos().x(), item.pos().y()) tr.translate(item.pos().x(), item.pos().y())
tr = tr * item.transform() tr = tr * item.transform()
if not item.isVisible() or int(item.flags() & item.ItemHasNoContents) > 0: #if not item.isVisible() or int(item.flags() & item.ItemHasNoContents) > 0:
m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
#print item, m ##print item, m
xmlStr = '<g transform="matrix(%f,%f,%f,%f,%f,%f)"></g>' % 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
else: #doc = xml.parseString(xmlStr)
#else:
arr = QtCore.QByteArray() arr = QtCore.QByteArray()
buf = QtCore.QBuffer(arr) buf = QtCore.QBuffer(arr)
svg = QtSvg.QSvgGenerator() svg = QtSvg.QSvgGenerator()
@ -116,63 +177,113 @@ class SVGExporter(Exporter):
if hasattr(item, 'setExportMode'): if hasattr(item, 'setExportMode'):
item.setExportMode(True, {'painter': p}) item.setExportMode(True, {'painter': p})
try: try:
#tr = QtGui.QTransform() #p.setTransform(tr)
#tr.translate(item.pos().x(), item.pos().y())
#p.setTransform(tr * item.transform())
p.setTransform(tr)
item.paint(p, QtGui.QStyleOptionGraphicsItem(), None) item.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
finally: finally:
p.end() p.end()
if hasattr(item, 'setExportMode'): ## Can't do this here--we need to wait until all children have painted as well.
item.setExportMode(False) ## this is taken care of in generateSvg instead.
#if hasattr(item, 'setExportMode'):
#item.setExportMode(False)
xmlStr = str(arr) xmlStr = str(arr)
childs = item.childItems()
doc = xml.parseString(xmlStr) doc = xml.parseString(xmlStr)
try: try:
groups = doc.getElementsByTagName('g') ## Get top-level group for this item
if len(groups) == 1: g1 = doc.getElementsByTagName('g')[0]
g1 = g2 = groups[0] ## get list of sub-groups
else: g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
g1,g2 = groups[:2]
except: except:
print doc.toxml() print doc.toxml()
raise raise
g1.setAttribute('id', item.__class__.__name__)
## Check for item visibility ## make sure g1 has the transformation matrix
visible = True m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
#print "=================",item,"====================="
#print g1.toprettyxml(indent=" ", newl='')
## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1)
## So we need to correct anything attempting to use this.
correctStroke(g1, item, root)
## 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): if not isinstance(item, QtGui.QGraphicsScene):
parent = item ## See if this item clips its children
while visible and parent is not None: if int(item.flags() & item.ItemClipsChildrenToShape) > 0:
visible = parent.isVisible() ## Generate svg for just the path
parent = parent.parentItem() path = QtGui.QGraphicsPathItem(item.shape())
pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
if not visible: ## and for the clipPath element
style = g1.getAttribute('style').strip() clip = name + '_clip'
if len(style)>0 and not style.endswith(';'): clipNode = g1.ownerDocument.createElement('clipPath')
style += ';' clipNode.setAttribute('id', clip)
style += 'display:none;' clipNode.appendChild(pathNode)
g1.setAttribute('style', style) 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()) childs.sort(key=lambda c: c.zValue())
for ch in childs: for ch in childs:
cg = self.generateItemSvg(ch) cg = _generateItemSvg(ch, nodes, root)
g2.appendChild(cg) 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 return g1
### To check: def correctStroke(node, item, root, width=1):
### do all items really generate this double-group structure? #print "==============", item, node
### are both groups necessary? if node.hasAttribute('stroke-width'):
### How do we implement clipping? (can we clip to an object that is visible?) 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)