2012-05-21 20:38:49 +00:00
|
|
|
"""
|
|
|
|
SVGDocument
|
|
|
|
"""
|
|
|
|
import wx
|
|
|
|
|
|
|
|
from cStringIO import StringIO
|
|
|
|
import warnings
|
|
|
|
import math
|
|
|
|
from functools import wraps
|
|
|
|
|
|
|
|
import pathdata
|
|
|
|
import css
|
|
|
|
from svg.css.colour import colourValue
|
|
|
|
from svg.css import values
|
|
|
|
from attributes import paintValue
|
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
document = """<?xml version = "1.0" standalone = "no"?>
|
2012-08-08 06:39:50 +00:00
|
|
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
2012-05-21 20:38:49 +00:00
|
|
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
2012-08-08 07:38:48 +00:00
|
|
|
<svg width = "4cm" height = "4cm" viewBox = "0 0 400 400"
|
|
|
|
xmlns = "http://www.w3.org/2000/svg" version = "1.1">
|
2012-05-21 20:38:49 +00:00
|
|
|
<title>Example triangle01- simple example of a 'path'</title>
|
|
|
|
<desc>A path that draws a triangle</desc>
|
2012-08-08 07:38:48 +00:00
|
|
|
<rect x = "1" y = "1" width = "398" height = "398"
|
|
|
|
fill = "none" stroke = "blue" />
|
|
|
|
<path d = "M 100 100 L 300 100 L 200 300 z"
|
|
|
|
fill = "red" stroke = "blue" stroke-width = "3" />
|
2012-05-21 20:38:49 +00:00
|
|
|
</svg>"""
|
|
|
|
|
|
|
|
makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
|
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
def attrAsFloat(node, attr, defaultValue = "0"):
|
2012-05-21 20:38:49 +00:00
|
|
|
val = node.get(attr, defaultValue)
|
|
|
|
#TODO: process stuff like "inherit" by walking back up the nodes
|
|
|
|
#fast path optimization - if it's a valid float, don't
|
|
|
|
#try to parse it.
|
|
|
|
try:
|
|
|
|
return float(val)
|
|
|
|
except ValueError:
|
|
|
|
return valueToPixels(val)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
def valueToPixels(val, defaultUnits = "px"):
|
2012-05-21 20:38:49 +00:00
|
|
|
#TODO manage default units
|
|
|
|
from pyparsing import ParseException
|
|
|
|
try:
|
|
|
|
val, unit = values.length.parseString(val)
|
|
|
|
except ParseException:
|
|
|
|
print "***", val
|
|
|
|
raise
|
|
|
|
#todo: unit conversion to something other than pixels
|
|
|
|
return val
|
|
|
|
|
|
|
|
|
|
|
|
def pathHandler(func):
|
|
|
|
"""decorator for methods which return a path operation
|
|
|
|
Creates the path they will fill,
|
|
|
|
and generates the path operations for the node
|
|
|
|
"""
|
|
|
|
@wraps(func)
|
|
|
|
def inner(self, node):
|
|
|
|
#brush = self.getBrushFromState()
|
|
|
|
#pen = self.getPenFromState()
|
|
|
|
#if not (brush or pen):
|
|
|
|
# return None, []
|
|
|
|
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
|
|
|
|
func(self, node, path)
|
|
|
|
ops = self.generatePathOps(path)
|
|
|
|
return path, ops
|
|
|
|
return inner
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
|
|
|
|
class SVGDocument(object):
|
|
|
|
lastControl = None
|
|
|
|
brushCache = {}
|
|
|
|
penCache = {}
|
|
|
|
def __init__(self, element):
|
|
|
|
"""
|
|
|
|
Create an SVG document from an ElementTree node.
|
|
|
|
"""
|
|
|
|
self.handlers = {
|
|
|
|
'{http://www.w3.org/2000/svg}svg':self.addGroupToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}a':self.addGroupToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}g':self.addGroupToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}rect':self.addRectToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}circle': self.addCircleToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}line': self.addLineToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}path':self.addPathDataToDocument,
|
|
|
|
'{http://www.w3.org/2000/svg}text':self.addTextToDocument
|
|
|
|
}
|
|
|
|
|
|
|
|
assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment'
|
|
|
|
self.tree = element
|
|
|
|
self.paths = {}
|
|
|
|
self.stateStack = [{}]
|
|
|
|
path, ops = self.processElement(element)
|
|
|
|
self.ops = ops
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
""" Retrieve the current state, without popping"""
|
|
|
|
return self.stateStack[-1]
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def processElement(self, element):
|
|
|
|
""" Process one element of the XML tree.
|
|
|
|
Returns the path representing the node,
|
|
|
|
and an operation list for drawing the node.
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
Parent nodes should return a path (for hittesting), but
|
|
|
|
no draw operations
|
|
|
|
"""
|
|
|
|
#copy the current state
|
|
|
|
current = dict(self.state)
|
|
|
|
current.update(element.items())
|
|
|
|
current.update(css.inlineStyle(element.get("style", "")))
|
|
|
|
self.stateStack.append(current)
|
|
|
|
handler = self.handlers.get(element.tag, lambda *any: (None, None))
|
|
|
|
path, ops = handler(element)
|
|
|
|
self.paths[element] = path
|
|
|
|
self.stateStack.pop()
|
|
|
|
return path, ops
|
|
|
|
|
|
|
|
def createTransformOpsFromNode(self, node):
|
|
|
|
""" Returns an oplist for transformations.
|
|
|
|
This applies to a node, not the current state because
|
|
|
|
the transform stack is saved in the wxGraphicsContext.
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
This oplist does *not* include the push/pop state commands
|
|
|
|
"""
|
|
|
|
ops = []
|
|
|
|
transform = node.get('transform')
|
|
|
|
#todo: replace this with a mapping list
|
|
|
|
if transform:
|
|
|
|
for transform, args in css.transformList.parseString(transform):
|
|
|
|
if transform == 'scale':
|
|
|
|
if len(args) == 1:
|
|
|
|
x = y = args[0]
|
|
|
|
else:
|
|
|
|
x, y = args
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.Scale, (x, y))
|
|
|
|
)
|
|
|
|
if transform == 'translate':
|
|
|
|
if len(args) == 1:
|
|
|
|
x = args[0]
|
|
|
|
y = 0
|
|
|
|
else:
|
|
|
|
x, y = args
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.Translate, (x, y))
|
|
|
|
)
|
|
|
|
if transform == 'rotate':
|
|
|
|
if len(args) == 3:
|
|
|
|
angle, cx, cy = args
|
|
|
|
angle = math.radians(angle)
|
|
|
|
ops.extend([
|
|
|
|
(wx.GraphicsContext.Translate, (cx, cy)),
|
|
|
|
(wx.GraphicsContext.Rotate, (angle,)),
|
|
|
|
(wx.GraphicsContext.Translate, (-cx, -cy)),
|
|
|
|
])
|
|
|
|
else:
|
|
|
|
angle = args[0]
|
|
|
|
angle = math.radians(angle)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.Rotate, (angle,))
|
|
|
|
)
|
|
|
|
if transform == 'matrix':
|
|
|
|
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
|
|
|
|
*args
|
|
|
|
)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.ConcatTransform, (matrix,))
|
|
|
|
)
|
|
|
|
if transform == 'skewX':
|
|
|
|
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
|
2012-08-08 07:58:09 +00:00
|
|
|
1, 0, math.tan(math.radians(args[0])), 1, 0, 0
|
2012-05-21 20:38:49 +00:00
|
|
|
)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.ConcatTransform, (matrix,))
|
|
|
|
)
|
|
|
|
if transform == 'skewY':
|
|
|
|
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
|
2012-08-08 07:58:09 +00:00
|
|
|
1, math.tan(math.radians(args[0])), 0, 1, 0, 0
|
2012-05-21 20:38:49 +00:00
|
|
|
)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.ConcatTransform, (matrix,))
|
|
|
|
)
|
|
|
|
return ops
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def addGroupToDocument(self, node):
|
|
|
|
""" For parent elements: push on a state,
|
|
|
|
then process all child elements
|
|
|
|
"""
|
|
|
|
ops = [
|
|
|
|
(wx.GraphicsContext.PushState, ())
|
|
|
|
]
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
path = makePath()
|
|
|
|
ops.extend(self.createTransformOpsFromNode(node))
|
|
|
|
for child in node.getchildren():
|
|
|
|
cpath, cops = self.processElement(child)
|
|
|
|
if cpath:
|
|
|
|
path.AddPath(cpath)
|
|
|
|
if cops:
|
|
|
|
ops.extend(cops)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.PopState, ())
|
|
|
|
)
|
|
|
|
return path, ops
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def getFontFromState(self):
|
|
|
|
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
|
|
|
|
family = self.state.get("font-family")
|
|
|
|
if False:#family:
|
|
|
|
#print "setting font", family
|
|
|
|
font.SetFaceName(family)
|
|
|
|
size = self.state.get("font-size")
|
|
|
|
#I'm not sure if this is right or not
|
|
|
|
if size:
|
|
|
|
val, unit = values.length.parseString(size)
|
|
|
|
if '__WXMSW__' in wx.PlatformInfo:
|
|
|
|
i = int(val)
|
|
|
|
font.SetPixelSize((i, i))
|
|
|
|
else:
|
|
|
|
font.SetPointSize(int(val))
|
|
|
|
return font
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def addTextToDocument(self, node):
|
|
|
|
x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')]
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
def DoDrawText(context, text, x, y, brush = wx.NullGraphicsBrush):
|
2012-05-21 20:38:49 +00:00
|
|
|
#SVG spec appears to originate text from the bottom
|
|
|
|
#rather than the top as with our API. This function
|
|
|
|
#will measure and then re-orient the text as needed.
|
|
|
|
w, h = context.GetTextExtent(text)
|
|
|
|
y -= h
|
|
|
|
context.DrawText(text, x, y, brush)
|
|
|
|
font = self.getFontFromState()
|
|
|
|
brush = self.getBrushFromState()
|
|
|
|
if not (brush and brush.IsOk()):
|
|
|
|
brush = wx.BLACK_BRUSH
|
|
|
|
#normalize whitespace
|
|
|
|
#TODO: SVG probably has rules for this
|
|
|
|
text = ' '.join(node.text.split() if node.text else "")
|
|
|
|
if text is None:
|
|
|
|
return None, []
|
|
|
|
ops = [
|
|
|
|
(wx.GraphicsContext.SetFont, (font, brush.Colour)),
|
|
|
|
(DoDrawText, (text, x, y))
|
|
|
|
]
|
|
|
|
return None, ops
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
@pathHandler
|
|
|
|
def addRectToDocument(self, node, path):
|
|
|
|
x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height'])
|
|
|
|
rx = node.get('rx')
|
|
|
|
ry = node.get('ry')
|
|
|
|
if not (w and h):
|
2012-08-08 07:38:48 +00:00
|
|
|
path.MoveToPoint(x, y) #keep the current point correct
|
2012-05-21 20:38:49 +00:00
|
|
|
return
|
|
|
|
if rx or ry:
|
|
|
|
if rx and ry:
|
|
|
|
rx, ry = float(rx), float(ry)
|
|
|
|
elif rx:
|
|
|
|
rx = ry = float(rx)
|
|
|
|
elif ry:
|
|
|
|
rx = ry = float(ry)
|
|
|
|
#value clamping as per spec section 9.2
|
|
|
|
rx = min(rx, w/2)
|
|
|
|
ry = min(ry, h/2)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#origin
|
|
|
|
path.MoveToPoint(x+rx, y)
|
|
|
|
path.AddLineToPoint(x+w-rx, y)
|
|
|
|
#top right
|
|
|
|
cx = rx * 2
|
|
|
|
cy = ry * 2
|
|
|
|
path.AddEllipticalArc(
|
|
|
|
x+w-cx, y,
|
|
|
|
cx, cy,
|
|
|
|
math.radians(270), math.radians(0),
|
|
|
|
True
|
|
|
|
)
|
|
|
|
path.AddLineToPoint(x+w, y+h-ry)
|
|
|
|
#bottom right
|
|
|
|
path.AddEllipticalArc(
|
|
|
|
x+w-cx, y+h-cy,
|
|
|
|
cx, cy,
|
|
|
|
math.radians(0), math.radians(90),
|
|
|
|
True
|
|
|
|
)
|
|
|
|
path.AddLineToPoint(x+rx, y+h)
|
|
|
|
#bottom left
|
|
|
|
path.AddEllipticalArc(
|
|
|
|
x, y+h-cy,
|
|
|
|
cx, cy,
|
|
|
|
math.radians(90),
|
|
|
|
math.radians(180),
|
|
|
|
True
|
|
|
|
)
|
|
|
|
path.AddLineToPoint(x, y+ry)
|
|
|
|
#bottom right
|
|
|
|
path.AddEllipticalArc(
|
|
|
|
x, y,
|
|
|
|
cx, cy,
|
|
|
|
math.radians(180),
|
|
|
|
math.radians(270),
|
|
|
|
True
|
|
|
|
)
|
|
|
|
path.CloseSubpath()
|
|
|
|
else:
|
|
|
|
path.AddRectangle(
|
|
|
|
x, y, w, h
|
|
|
|
)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
@pathHandler
|
|
|
|
def addCircleToDocument(self, node, path):
|
|
|
|
cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')]
|
|
|
|
path.AddCircle(cx, cy, r)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
@pathHandler
|
|
|
|
def addEllipseToDocument(self, node, path):
|
|
|
|
cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')]
|
|
|
|
#cx, cy are centerpoint.
|
|
|
|
#rx, ry are radius.
|
|
|
|
#wxGC coords are the rect of the ellipse bounding box.
|
|
|
|
if rx <= 0 or ry <= 0:
|
|
|
|
return
|
|
|
|
x = cx - rx
|
|
|
|
y = cy - ry
|
|
|
|
path.AddEllipse(x, y, rx*2, ry*2)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
@pathHandler
|
2012-05-21 20:38:49 +00:00
|
|
|
def addLineToDocument(self, node, path):
|
|
|
|
x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')]
|
|
|
|
path.MoveToPoint(x1, y1)
|
|
|
|
path.AddLineToPoint(x2, y2)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
@pathHandler
|
|
|
|
def addPolyLineToDocument(self, node, path):
|
|
|
|
#translate to pathdata and render that
|
|
|
|
data = "M " + node.get("points")
|
|
|
|
self.addPathDataToPath(data, path)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
@pathHandler
|
2012-05-21 20:38:49 +00:00
|
|
|
def addPolygonToDocument(self, node, path):
|
|
|
|
#translate to pathdata and render that
|
|
|
|
data = "M " + node.get("points") + " Z"
|
|
|
|
self.addPathDataToPath(data, path)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
@pathHandler
|
|
|
|
def addPathDataToDocument(self, node, path):
|
|
|
|
self.addPathDataToPath(node.get('d', ''), path)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def addPathDataToPath(self, data, path):
|
|
|
|
self.lastControl = None
|
|
|
|
self.lastControlQ = None
|
|
|
|
self.firstPoints = []
|
|
|
|
def normalizeStrokes(parseResults):
|
|
|
|
""" The data comes from the parser in the
|
|
|
|
form of (command, [list of arguments]).
|
|
|
|
We translate that to [(command, args[0]), (command, args[1])]
|
|
|
|
via a generator.
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
M is special cased because its subsequent arguments
|
|
|
|
become linetos.
|
|
|
|
"""
|
|
|
|
for command, arguments in parseResults:
|
|
|
|
if not arguments:
|
|
|
|
yield (command, ())
|
|
|
|
else:
|
|
|
|
arguments = iter(arguments)
|
|
|
|
if command == 'm':
|
|
|
|
yield (command, arguments.next())
|
|
|
|
command = "l"
|
|
|
|
elif command == "M":
|
|
|
|
yield (command, arguments.next())
|
|
|
|
command = "L"
|
|
|
|
for arg in arguments:
|
|
|
|
yield (command, arg)
|
|
|
|
#print "data length", len(data)
|
|
|
|
import time
|
|
|
|
t = time.time()
|
|
|
|
parsed = pathdata.svg.parseString(data)
|
|
|
|
#print "parsed in", time.time()-t
|
|
|
|
for stroke in normalizeStrokes(parsed):
|
|
|
|
self.addStrokeToPath(path, stroke)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def generatePathOps(self, path):
|
2012-08-08 06:39:50 +00:00
|
|
|
""" Look at the current state and generate the
|
2012-05-21 20:38:49 +00:00
|
|
|
draw operations (fill, stroke, neither) for the path"""
|
|
|
|
ops = []
|
|
|
|
brush = self.getBrushFromState(path)
|
|
|
|
fillRule = self.state.get('fill-rule', 'nonzero')
|
|
|
|
frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE}
|
|
|
|
fr = frMap.get(fillRule, wx.ODDEVEN_RULE)
|
|
|
|
if brush:
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.SetBrush, (brush,))
|
|
|
|
)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.FillPath, (path, fr))
|
|
|
|
)
|
|
|
|
pen = self.getPenFromState()
|
|
|
|
if pen:
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.SetPen, (pen,))
|
|
|
|
)
|
|
|
|
ops.append(
|
|
|
|
(wx.GraphicsContext.StrokePath, (path,))
|
|
|
|
)
|
|
|
|
return ops
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def getPenFromState(self):
|
|
|
|
pencolour = self.state.get('stroke', 'none')
|
|
|
|
if pencolour == 'currentColor':
|
|
|
|
pencolour = self.state.get('color', 'none')
|
|
|
|
if pencolour == 'transparent':
|
|
|
|
return wx.TRANSPARENT_PEN
|
|
|
|
if pencolour == 'none':
|
|
|
|
return wx.NullPen
|
|
|
|
type, value = colourValue.parseString(pencolour)
|
|
|
|
if type == 'URL':
|
|
|
|
warnings.warn("Color servers for stroking not implemented")
|
|
|
|
return wx.NullPen
|
|
|
|
else:
|
|
|
|
if value[:3] == (-1, -1, -1):
|
|
|
|
return wx.NullPen
|
|
|
|
pen = wx.Pen(wx.Colour(*value))
|
|
|
|
width = self.state.get('stroke-width')
|
|
|
|
if width:
|
|
|
|
width, units = values.length.parseString(width)
|
|
|
|
pen.SetWidth(width)
|
|
|
|
capmap = {
|
|
|
|
'butt':wx.CAP_BUTT,
|
|
|
|
'round':wx.CAP_ROUND,
|
|
|
|
'square':wx.CAP_PROJECTING
|
|
|
|
}
|
|
|
|
joinmap = {
|
|
|
|
'miter':wx.JOIN_MITER,
|
|
|
|
'round':wx.JOIN_ROUND,
|
|
|
|
'bevel':wx.JOIN_BEVEL
|
|
|
|
}
|
|
|
|
pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT))
|
|
|
|
pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER))
|
|
|
|
return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen)
|
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
def getBrushFromState(self, path = None):
|
2012-05-21 20:38:49 +00:00
|
|
|
brushcolour = self.state.get('fill', 'black').strip()
|
|
|
|
type, details = paintValue.parseString(brushcolour)
|
|
|
|
if type == "URL":
|
|
|
|
url, fallback = details
|
|
|
|
element = self.resolveURL(url)
|
|
|
|
if not element:
|
|
|
|
if fallback:
|
|
|
|
type, details = fallback
|
|
|
|
else:
|
|
|
|
r, g, b, = 0, 0, 0
|
|
|
|
else:
|
|
|
|
if element.tag == '{http://www.w3.org/2000/svg}linearGradient':
|
|
|
|
box = path.GetBox()
|
|
|
|
x, y, w, h = box.Get()
|
|
|
|
return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush(
|
2012-08-08 07:58:09 +00:00
|
|
|
x, y, x+w, y+h, wx.Colour(0, 0, 255, 128), wx.RED
|
2012-05-21 20:38:49 +00:00
|
|
|
)
|
|
|
|
elif element.tag == '{http://www.w3.org/2000/svg}radialGradient':
|
|
|
|
box = path.GetBox()
|
|
|
|
x, y, w, h = box.Get()
|
|
|
|
#print w
|
2012-08-08 07:58:09 +00:00
|
|
|
mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x, y, w, h)
|
2012-05-21 20:38:49 +00:00
|
|
|
cx, cy = mx.TransformPoint(0.5, 0.5)
|
|
|
|
fx, fy = cx, cy
|
|
|
|
return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush(
|
2012-08-08 07:38:48 +00:00
|
|
|
cx, cy,
|
|
|
|
fx, fy,
|
|
|
|
(max(w, h))/2,
|
2012-08-08 07:58:09 +00:00
|
|
|
wx.Colour(0, 0, 255, 128), wx.RED
|
2012-05-21 20:38:49 +00:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
#invlid gradient specified
|
|
|
|
return wx.NullBrush
|
2012-08-08 07:58:09 +00:00
|
|
|
r, g, b = 0, 0, 0
|
2012-05-21 20:38:49 +00:00
|
|
|
if type == 'CURRENTCOLOR':
|
|
|
|
type, details = paintValue.parseString(self.state.get('color', 'none'))
|
|
|
|
if type == 'RGB':
|
2012-08-08 07:58:09 +00:00
|
|
|
r, g, b = details
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == "NONE":
|
|
|
|
return wx.NullBrush
|
|
|
|
opacity = self.state.get('fill-opacity', self.state.get('opacity', '1'))
|
|
|
|
opacity = float(opacity)
|
|
|
|
opacity = min(max(opacity, 0.0), 1.0)
|
|
|
|
a = 255 * opacity
|
2012-08-08 06:39:50 +00:00
|
|
|
#using try/except block instead of
|
2012-05-21 20:38:49 +00:00
|
|
|
#just setdefault because the wxBrush and wxColour would
|
|
|
|
#be created every time anyway in order to pass them,
|
|
|
|
#defeating the purpose of the cache
|
|
|
|
try:
|
2012-08-08 07:58:09 +00:00
|
|
|
return SVGDocument.brushCache[(r, g, b, a)]
|
2012-05-21 20:38:49 +00:00
|
|
|
except KeyError:
|
2012-08-08 07:58:09 +00:00
|
|
|
return SVGDocument.brushCache.setdefault((r, g, b, a), wx.Brush(wx.Colour(r, g, b, a)))
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def resolveURL(self, urlData):
|
|
|
|
"""
|
|
|
|
Resolve a URL and return an elementTree Element from it.
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
Return None if unresolvable
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
"""
|
|
|
|
scheme, netloc, path, query, fragment = urlData
|
|
|
|
if scheme == netloc == path == '':
|
|
|
|
#horrible. There's got to be a better way?
|
|
|
|
if self.tree.get("id") == fragment:
|
|
|
|
return self.tree
|
|
|
|
else:
|
|
|
|
for child in self.tree.getiterator():
|
|
|
|
#print child.get("id")
|
|
|
|
if child.get("id") == fragment:
|
|
|
|
return child
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return self.resolveRemoteURL(urlData)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def resolveRemoteURL(self, url):
|
|
|
|
return None
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def addStrokeToPath(self, path, stroke):
|
|
|
|
""" Given a stroke from a path command
|
|
|
|
(in the form (command, arguments)) create the path
|
|
|
|
commands that represent it.
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
TODO: break out into (yet another) class/module,
|
|
|
|
especially so we can get O(1) dispatch on type?
|
|
|
|
"""
|
|
|
|
type, arg = stroke
|
|
|
|
relative = False
|
|
|
|
if type == type.lower():
|
|
|
|
relative = True
|
|
|
|
ox, oy = path.GetCurrentPoint().Get()
|
|
|
|
else:
|
|
|
|
ox = oy = 0
|
|
|
|
def normalizePoint(arg):
|
|
|
|
x, y = arg
|
|
|
|
return x+ox, y+oy
|
|
|
|
def reflectPoint(point, relativeTo):
|
|
|
|
x, y = point
|
|
|
|
a, b = relativeTo
|
|
|
|
return ((a*2)-x), ((b*2)-y)
|
|
|
|
type = type.upper()
|
|
|
|
if type == 'M':
|
|
|
|
pt = normalizePoint(arg)
|
|
|
|
self.firstPoints.append(pt)
|
|
|
|
path.MoveToPoint(pt)
|
|
|
|
elif type == 'L':
|
|
|
|
path.AddLineToPoint(normalizePoint(arg))
|
|
|
|
elif type == 'C':
|
|
|
|
#control1, control2, endpoint = arg
|
|
|
|
control1, control2, endpoint = map(
|
|
|
|
normalizePoint, arg
|
|
|
|
)
|
|
|
|
self.lastControl = control2
|
|
|
|
path.AddCurveToPoint(
|
2012-08-08 06:39:50 +00:00
|
|
|
control1,
|
|
|
|
control2,
|
2012-05-21 20:38:49 +00:00
|
|
|
endpoint
|
|
|
|
)
|
|
|
|
#~ cp = path.GetCurrentPoint()
|
|
|
|
#~ path.AddCircle(c1x, c1y, 5)
|
|
|
|
#~ path.AddCircle(c2x, c2y, 3)
|
2012-08-08 07:38:48 +00:00
|
|
|
#~ path.AddCircle(x, y, 7)
|
2012-05-21 20:38:49 +00:00
|
|
|
#~ path.MoveToPoint(cp)
|
|
|
|
#~ print "C", control1, control2, endpoint
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == 'S':
|
|
|
|
#control2, endpoint = arg
|
|
|
|
control2, endpoint = map(
|
|
|
|
normalizePoint, arg
|
|
|
|
)
|
|
|
|
if self.lastControl:
|
|
|
|
control1 = reflectPoint(self.lastControl, path.GetCurrentPoint())
|
|
|
|
else:
|
|
|
|
control1 = path.GetCurrentPoint()
|
2012-08-08 07:58:09 +00:00
|
|
|
#~ print "S", self.lastControl, ":", control1, control2, endpoint
|
2012-05-21 20:38:49 +00:00
|
|
|
self.lastControl = control2
|
|
|
|
path.AddCurveToPoint(
|
2012-08-08 06:39:50 +00:00
|
|
|
control1,
|
|
|
|
control2,
|
2012-05-21 20:38:49 +00:00
|
|
|
endpoint
|
|
|
|
)
|
|
|
|
elif type == "Q":
|
2012-08-08 07:38:48 +00:00
|
|
|
(cx, cy), (x, y) = map(normalizePoint, arg)
|
2012-05-21 20:38:49 +00:00
|
|
|
self.lastControlQ = (cx, cy)
|
|
|
|
path.AddQuadCurveToPoint(cx, cy, x, y)
|
|
|
|
elif type == "T":
|
|
|
|
x, y, = normalizePoint(arg)
|
|
|
|
if self.lastControlQ:
|
|
|
|
cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint())
|
|
|
|
else:
|
|
|
|
cx, cy = path.GetCurrentPoint()
|
|
|
|
self.lastControlQ = (cx, cy)
|
|
|
|
path.AddQuadCurveToPoint(cx, cy, x, y)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == "V":
|
|
|
|
_, y = normalizePoint((0, arg))
|
|
|
|
x, _ = path.GetCurrentPoint()
|
2012-08-08 07:38:48 +00:00
|
|
|
path.AddLineToPoint(x, y)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == "H":
|
|
|
|
x, _ = normalizePoint((arg, 0))
|
|
|
|
_, y = path.GetCurrentPoint()
|
2012-08-08 07:38:48 +00:00
|
|
|
path.AddLineToPoint(x, y)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == "A":
|
|
|
|
#wxGC currently only supports circular arcs,
|
|
|
|
#not eliptical ones
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
(
|
|
|
|
(rx, ry), #radii of ellipse
|
|
|
|
angle, #angle of rotation on the ellipse in degrees
|
|
|
|
(fa, fs), #arc and stroke angle flags
|
|
|
|
(x, y) #endpoint on the arc
|
2012-08-08 06:39:50 +00:00
|
|
|
) = arg
|
|
|
|
|
2012-08-08 07:38:48 +00:00
|
|
|
x, y = normalizePoint((x, y))
|
2012-05-21 20:38:49 +00:00
|
|
|
cx, cy = path.GetCurrentPoint()
|
|
|
|
if (cx, cy) == (x, y):
|
|
|
|
return #noop
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
if (rx == 0 or ry == 0):
|
|
|
|
#no radius is effectively a line
|
2012-08-08 07:38:48 +00:00
|
|
|
path.AddLineToPoint(x, y)
|
2012-05-21 20:38:49 +00:00
|
|
|
return
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#find the center point for the ellipse
|
|
|
|
#translation via the angle
|
|
|
|
angle = angle % 360
|
|
|
|
angle = math.radians(angle)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#translated endpoint
|
|
|
|
xPrime = math.cos(angle) * ((cx-x)/2)
|
|
|
|
xPrime += math.sin(angle) * ((cx-x)/2)
|
|
|
|
yPrime = -(math.sin(angle)) * ((cy-y)/2)
|
|
|
|
yPrime += (math.cos(angle)) * ((cy-y)/2)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2))
|
|
|
|
temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2))
|
|
|
|
temp = abs(temp)
|
|
|
|
try:
|
|
|
|
temp = math.sqrt(temp)
|
|
|
|
except ValueError:
|
|
|
|
import pdb
|
|
|
|
pdb.set_trace()
|
|
|
|
cxPrime = temp * ((rx * yPrime) / ry)
|
|
|
|
cyPrime = temp * -((ry * xPrime) / rx)
|
|
|
|
if fa == fs:
|
|
|
|
cxPrime, cyPrime = -cxPrime, -cyPrime
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#reflect backwards now for the origin
|
|
|
|
cnx = math.cos(angle) * cxPrime
|
|
|
|
cnx += math.sin(angle) * cxPrime
|
|
|
|
cny = -(math.sin(angle)) * cyPrime
|
|
|
|
cny += (math.cos(angle)) * cyPrime
|
|
|
|
cnx += ((cx+x)/2.0)
|
|
|
|
cny += ((cy+y)/2.0)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#calculate the angle between the two endpoints
|
|
|
|
lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle()
|
|
|
|
firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle()
|
|
|
|
lastArc = math.radians(lastArc)
|
|
|
|
firstArc = math.radians(firstArc)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
#aargh buggines.
|
|
|
|
#AddArc draws a straight line between
|
|
|
|
#the endpoints of the arc.
|
2012-08-08 06:39:50 +00:00
|
|
|
#putting it in a subpath makes the strokes come out
|
2012-05-21 20:38:49 +00:00
|
|
|
#correctly, but it still only fills the slice
|
|
|
|
#taking out the MoveToPoint fills correctly.
|
|
|
|
path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2)
|
|
|
|
path.MoveToPoint(x, y)
|
|
|
|
#~ npath = makePath()
|
|
|
|
#~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False)
|
2012-08-08 07:38:48 +00:00
|
|
|
#~ npath.MoveToPoint(x, y)
|
2012-05-21 20:38:49 +00:00
|
|
|
#~ path.AddPath(npath)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
elif type == 'Z':
|
|
|
|
#~ Bugginess:
|
|
|
|
#~ CloseSubpath() doesn't change the
|
|
|
|
#~ current point, as SVG spec requires.
|
|
|
|
#~ However, manually moving to the endpoint afterward opens a new subpath
|
|
|
|
#~ and (apparently) messes with stroked but not filled paths.
|
|
|
|
#~ This is possibly a bug in GDI+?
|
|
|
|
#~ Manually closing the path via AddLineTo gives incorrect line join
|
|
|
|
#~ results
|
|
|
|
#~ Manually closing the path *and* calling CloseSubpath() appears
|
|
|
|
#~ to give correct results on win32
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
pt = self.firstPoints.pop()
|
|
|
|
path.AddLineToPoint(pt)
|
|
|
|
path.CloseSubpath()
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
def render(self, context):
|
|
|
|
if not hasattr(self, "ops"):
|
|
|
|
return
|
|
|
|
for op, args in self.ops:
|
|
|
|
op(context, *args)
|
2012-08-08 06:39:50 +00:00
|
|
|
|
2012-05-21 20:38:49 +00:00
|
|
|
if __name__ == '__main__':
|
|
|
|
from tests.test_document import *
|
|
|
|
unittest.main()
|