diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py
index 09dc7c58..6e950770 100644
--- a/pyqtgraph/__init__.py
+++ b/pyqtgraph/__init__.py
@@ -50,7 +50,7 @@ CONFIG_OPTIONS = {
def setConfigOption(opt, value):
CONFIG_OPTIONS[opt] = value
-def setConfigOptions(opts):
+def setConfigOptions(**opts):
CONFIG_OPTIONS.update(opts)
def getConfigOption(opt):
diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py
index bac03dae..4052deec 100644
--- a/pyqtgraph/exporters/SVGExporter.py
+++ b/pyqtgraph/exporters/SVGExporter.py
@@ -1,6 +1,7 @@
from .Exporter import Exporter
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
+import pyqtgraph as pg
import re
import xml.dom.minidom as xml
@@ -72,107 +73,217 @@ class SVGExporter(Exporter):
##print "old line:", line
##print "new line:", data[i]
#open(fileName, 'w').write(''.join(data))
-
- node = self.generateItemSvg(self.item)
- xml = """\
-
-\n"
-
+
+ ## Qt's SVG generator is not complete. (notably, it lacks clipping)
+ ## Instead, we will use Qt to generate SVG for each item independently,
+ ## then manually reconstruct the entire document.
+ xml = generateSvg(self.item)
+
if toBytes:
return bytes(xml)
else:
with open(fileName, 'w') as fh:
fh.write(xml)
- def generateItemSvg(self, item):
+xmlHeader = """\
+
+\n"
- p = QtGui.QPainter()
- 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)
- 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 = "\n\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 = 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 = '' % 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:
- groups = doc.getElementsByTagName('g')
- if len(groups) == 1:
- g1 = g2 = groups[0]
- else:
- g1,g2 = groups[:2]
- except:
- print doc.toxml()
- raise
- g1.setAttribute('id', item.__class__.__name__)
-
- ## Check for item visibility
- visible = True
- if not isinstance(item, QtGui.QGraphicsScene):
- parent = item
- while visible and parent is not None:
- visible = parent.isVisible()
- parent = parent.parentItem()
+ #p.setTransform(tr)
+ item.paint(p, QtGui.QStyleOptionGraphicsItem(), None)
+ finally:
+ p.end()
+ ## Can't do this here--we need to wait until all children have painted as well.
+ ## this is taken care of in generateSvg instead.
+ #if hasattr(item, 'setExportMode'):
+ #item.setExportMode(False)
- if not visible:
- style = g1.getAttribute('style').strip()
- if len(style)>0 and not style.endswith(';'):
- style += ';'
- style += 'display:none;'
- g1.setAttribute('style', style)
-
- childs.sort(key=lambda c: c.zValue())
- for ch in childs:
- cg = self.generateItemSvg(ch)
- g2.appendChild(cg)
+ xmlStr = str(arr)
+ doc = xml.parseString(xmlStr)
- return g1
-
-
-
- ### To check:
- ### do all items really generate this double-group structure?
- ### are both groups necessary?
- ### How do we implement clipping? (can we clip to an object that is visible?)
-
-
-
+ try:
+ ## Get top-level group for this item
+ g1 = doc.getElementsByTagName('g')[0]
+ ## get list of sub-groups
+ g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
+ except:
+ print doc.toxml()
+ raise
+
+ ## make sure g1 has the transformation matrix
+ 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):
+ ## 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)
diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py
index 5a389616..3752a6bb 100644
--- a/pyqtgraph/widgets/RemoteGraphicsView.py
+++ b/pyqtgraph/widgets/RemoteGraphicsView.py
@@ -21,7 +21,7 @@ class RemoteGraphicsView(QtGui.QWidget):
QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess()
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')
self._view = rpgRemote.Renderer(*args, **kwds)
self._view._setProxyOptions(deferGetattr=True)
diff --git a/tests/svg.py b/tests/svg.py
index dc9657bb..db54eb83 100644
--- a/tests/svg.py
+++ b/tests/svg.py
@@ -7,20 +7,39 @@ app = pg.mkQApp()
class SVGTest(test.TestCase):
def test_plotscene(self):
- p = pg.plot([1,5,2,3,4,6,1,2,4,2,3,5,3])
- p.setXRange(0,5)
- ex = pg.exporters.SVGExporter.SVGExporter(p.scene())
+ pg.setConfigOption('foreground', (0,0,0))
+ w = pg.GraphicsWindow()
+ 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')
#def test_simple(self):
#rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100)
- ##rect.rotate(30)
+ #rect.translate(50, 50)
+ #rect.rotate(30)
#grp = pg.ItemGroup()
#grp.setParentItem(rect)
#grp.translate(200,0)
- #grp.rotate(30)
- #el = pg.QtGui.QGraphicsEllipseItem(10, 0, 100, 50)
- #el.setParentItem(grp)
+ ##grp.rotate(30)
+
+ #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.export(fileName='test.svg')