svg exporter updates
This commit is contained in:
parent
af59296231
commit
b0e0781624
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -72,107 +73,217 @@ class SVGExporter(Exporter):
|
|||||||
##print "old line:", line
|
##print "old line:", line
|
||||||
##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)
|
||||||
else:
|
else:
|
||||||
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:
|
cleanXml(node)
|
||||||
arr = QtCore.QByteArray()
|
|
||||||
buf = QtCore.QBuffer(arr)
|
return xmlHeader + node.toprettyxml(indent=' ') + "\n</svg>\n"
|
||||||
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:
|
|
||||||
#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 = "<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
|
|
||||||
visible = True
|
|
||||||
if not isinstance(item, QtGui.QGraphicsScene):
|
|
||||||
parent = item
|
|
||||||
while visible and parent is not None:
|
|
||||||
visible = parent.isVisible()
|
|
||||||
parent = parent.parentItem()
|
|
||||||
|
|
||||||
if not visible:
|
xmlStr = str(arr)
|
||||||
style = g1.getAttribute('style').strip()
|
doc = xml.parseString(xmlStr)
|
||||||
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)
|
|
||||||
|
|
||||||
return g1
|
try:
|
||||||
|
## Get top-level group for this item
|
||||||
|
g1 = doc.getElementsByTagName('g')[0]
|
||||||
|
## get list of sub-groups
|
||||||
### To check:
|
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
|
||||||
### do all items really generate this double-group structure?
|
except:
|
||||||
### are both groups necessary?
|
print doc.toxml()
|
||||||
### How do we implement clipping? (can we clip to an object that is visible?)
|
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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
33
tests/svg.py
33
tests/svg.py
@ -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')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user