evil-mad-EggBot/inkscape_contributed/eggbot_stretch.py

674 wiersze
19 KiB
Python

#!/usr/bin/env python
# Written by Daniel C. Newman ( dan dot newman at mtbaldy dot us )
# 19 October 2010
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import math
import inkex
import simplepath
import simplestyle
import simpletransform
import cubicsuperpath
import cspsubdiv
import bezmisc
N_PAGE_WIDTH = 3200
N_PAGE_HEIGHT = 800
def inverseTransform ( tran ):
'''
An SVG transform matrix looks like
[ a c e ]
[ b d f ]
[ 0 0 1 ]
And it's inverse is
[ d -c cf - de ]
[ -b a be - af ] * ( ad - bc ) ** -1
[ 0 0 1 ]
And, no reasonable 2d coordinate transform will have
the products ad and bc equal.
SVG represents the transform matrix column by column as
matrix(a b c d e f) while Inkscape extensions store the
transform matrix as
[[a, c, e], [b, d, f]]
To invert the transform stored Inskcape style, we wish to
produce
[[d/D, -c/D, (cf - de)/D], [-b/D, a/D, (be-af)/D]]
where
D = 1 / (ad - bc)
'''
D = tran[0][0] * tran[1][1] - tran[1][0] * tran[0][1]
if D == 0:
return None
return [[tran[1][1]/D, -tran[0][1]/D,
(tran[0][1]*tran[1][2] - tran[1][1]*tran[0][2])/D],
[-tran[1][0]/D, tran[0][0]/D,
(tran[1][0]*tran[0][2] - tran[0][0]*tran[1][2])/D]]
def parseLengthWithUnits( str ):
'''
Parse an SVG value which may or may not have units attached
This version is greatly simplified in that it only allows: no units,
units of px, and units of %. Everything else, it returns None for.
There is a more general routine to consider in scour.py if more
generality is ever needed.
'''
u = 'px'
s = str.strip()
if s[-2:] == 'px':
s = s[:-2]
elif s[-1:] == '%':
u = '%'
s = s[:-1]
try:
v = float( s )
except:
return None, None
return v, u
def subdivideCubicPath( sp, flat, i=1 ):
'''
[ Lifted from eggbot.py with impunity ]
Break up a bezier curve into smaller curves, each of which
is approximately a straight line within a given tolerance
(the "smoothness" defined by [flat]).
This is a modified version of cspsubdiv.cspsubdiv(): rewritten
because recursion-depth errors on complicated line segments
could occur with cspsubdiv.cspsubdiv().
'''
while True:
while True:
if i >= len( sp ):
return
p0 = sp[i - 1][1]
p1 = sp[i - 1][2]
p2 = sp[i][0]
p3 = sp[i][1]
b = ( p0, p1, p2, p3 )
if cspsubdiv.maxdist( b ) > flat:
break
i += 1
one, two = bezmisc.beziersplitatt( b, 0.5 )
sp[i - 1][2] = one[1]
sp[i][0] = two[2]
p = [one[2], one[3], two[1]]
sp[i:1] = [p]
class Map( inkex.Effect ):
def __init__( self ):
inkex.Effect.__init__( self )
self.OptionParser.add_option('--smoothness', dest='smoothness',
type='float', default=float( 0.2 ), action='store',
help='Curve smoothing (less for more)' )
self.OptionParser.add_option('--maxDy', dest='maxDy',
type='float', default=float( 5.0 ), action='store',
help='Vertical smoothing (less for more)' )
self.cx = float( N_PAGE_WIDTH ) / 2.0
self.cy = float( N_PAGE_HEIGHT ) / 2.0
self.xmin, self.xmax = ( 1.0E70, -1.0E70 )
self.maxDy = float( 5 )
self.paths = {}
self.transforms = {}
# For handling an SVG viewbox attribute, we will need to know the
# values of the document's <svg> width and height attributes as well
# as establishing a transform from the viewbox to the display.
self.docWidth = float( N_PAGE_WIDTH )
self.docHeight = float( N_PAGE_HEIGHT )
self.docTransform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
def getLength( self, name, default ):
'''
Get the <svg> attribute with name "name" and default value "default"
Parse the attribute into a value and associated units. Then, accept
no units (''), units of pixels ('px'), and units of percentage ('%').
'''
str = self.document.getroot().get( name )
if str:
v, u = parseLengthWithUnits( str )
if not v:
# Couldn't parse the value
return None
elif ( u == '' ) or ( u == 'px' ):
return v
elif u == '%':
return float( default ) * v / 100.0
else:
# Unsupported units
return None
else:
# No width specified; assume the default value
return float( default )
def getDocProps( self ):
'''
Get the document's height and width attributes from the <svg> tag.
Use a default value in case the property is not present or is
expressed in units of percentages.
'''
self.docHeight = self.getLength( 'height', N_PAGE_HEIGHT )
self.docWidth = self.getLength( 'width', N_PAGE_WIDTH )
if ( self.docHeight == None ) or ( self.docWidth == None ):
return False
else:
return True
def handleViewBox( self ):
'''
Set up the document-wide transform in the event that the document has an SVG viewbox
'''
if self.getDocProps():
viewbox = self.document.getroot().get( 'viewBox' )
if viewbox:
vinfo = viewbox.strip().replace( ',', ' ' ).split( ' ' )
if ( vinfo[2] != 0 ) and ( vinfo[3] != 0 ):
sx = self.docWidth / float( vinfo[2] )
sy = self.docHeight / float( vinfo[3] )
self.docTransform = simpletransform.parseTransform( 'scale(%f,%f)' % (sx, sy) )
def getPathVertices( self, path, node=None, transform=None, find_bbox=False ):
'''
Decompose the path data from an SVG element into individual
subpaths, each subpath consisting of absolute move to and line
to coordinates. Place these coordinates into a list of polygon
vertices.
'''
if ( not path ) or ( len( path ) == 0 ):
# Nothing to do
return None
# parsePath() may raise an exception. This is okay
sp = simplepath.parsePath( path )
if ( not sp ) or ( len( sp ) == 0 ):
# Path must have been devoid of any real content
return None
# Get a cubic super path
p = cubicsuperpath.CubicSuperPath( sp )
if ( not p ) or ( len( p ) == 0 ):
# Probably never happens, but...
return None
if transform:
simpletransform.applyTransformToPath( transform, p )
# Now traverse the cubic super path
subpath_list = []
subpath_vertices = []
for sp in p:
if len( subpath_vertices ):
subpath_list.append( subpath_vertices )
subpath_vertices = []
last_csp = None
subdivideCubicPath( sp, float( self.options.smoothness ) )
for csp in sp:
if ( last_csp != None ) and ( math.fabs( csp[1][1] - last_csp[1] ) > self.options.maxDy ):
dy = ( csp[1][1] - last_csp[1] )
dx = ( csp[1][0] - last_csp[0] )
nsteps = math.ceil( math.fabs( dy / self.options.maxDy ) )
for n in range( 1, int( 1 + nsteps ) ):
s = n / nsteps
subpath_vertices.append( [ last_csp[0] + s * dx, last_csp[1] + s * dy ] )
else:
# Add this vertex to the list of vetices
subpath_vertices.append( csp[1] )
last_csp = csp[1]
if find_bbox:
if last_csp[0] < self.xmin:
self.xmin = last_csp[0]
elif last_csp[0] > self.xmax:
self.xmax = last_csp[0]
# Handle final subpath
if len( subpath_vertices ):
subpath_list.append( subpath_vertices )
if len( subpath_list ) > 0:
self.paths[node] = subpath_list
self.transforms[node] = transform
def mapPathVertices( self, node ):
steps2rads = math.pi / float( 1600 )
transform = self.transforms[node]
if transform is None:
invTransform = None
else:
invTransform = inverseTransform( transform )
newPath = ''
for subpath in self.paths[node]:
lastPoint = subpath[0]
lastPoint[0] = self.cx + ( lastPoint[0] - self.cx ) / math.cos( ( lastPoint[1] - self.cy ) * steps2rads )
if invTransform != None:
simpletransform.applyTransformToPoint( invTransform, lastPoint )
newPath += ' M %f,%f' % ( lastPoint[0], lastPoint[1] )
for point in subpath[1:]:
x = self.cx + ( point[0] - self.cx ) / math.cos( ( point[1] - self.cy ) * steps2rads )
pt = [x, point[1] ]
if invTransform != None:
simpletransform.applyTransformToPoint( invTransform, pt )
newPath += ' l %f,%f' % ( pt[0] - lastPoint[0], pt[1] - lastPoint[1] )
lastPoint = pt
self.paths[node] = newPath
def recursivelyTraverseSvg( self, aNodeList,
matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
parent_visibility='visible', find_bbox=False ):
'''
[ This too is largely lifted from eggbot.py ]
Recursively walk the SVG document, building polygon vertex lists
for each graphical element we support.
Rendered SVG elements:
<circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>
Supported SVG elements:
<group>, <use>
Ignored SVG elements:
<defs>, <eggbot>, <metadata>, <namedview>, <pattern>,
processing directives
All other SVG elements trigger an error (including <text>)
'''
for node in aNodeList:
# Ignore invisible nodes
v = node.get( 'visibility', parent_visibility )
if v == 'inherit':
v = parent_visibility
if v == 'hidden' or v == 'collapse':
pass
# First apply the current matrix transform to this node's tranform
matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) )
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
self.recursivelyTraverseSvg( node, matNew, v, find_bbox )
elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use':
# A <use> element refers to another SVG element via an xlink:href="#blah"
# attribute. We will handle the element by doing an XPath search through
# the document, looking for the element with the matching id="blah"
# attribute. We then recursively process that element after applying
# any necessary (x,y) translation.
#
# Notes:
# 1. We ignore the height and width attributes as they do not apply to
# path-like elements, and
# 2. Even if the use element has visibility="hidden", SVG still calls
# for processing the referenced element. The referenced element is
# hidden only if its visibility is "inherit" or "hidden".
refid = node.get( inkex.addNS( 'href', 'xlink' ) )
if not refid:
pass
# [1:] to ignore leading '#' in reference
path = '//*[@id="%s"]' % refid[1:]
refnode = node.xpath( path )
if refnode:
x = float( node.get( 'x', '0' ) )
y = float( node.get( 'y', '0' ) )
# Note: the transform has already been applied
if ( x != 0 ) or (y != 0 ):
matNew2 = composeTransform( matNew, parseTransform( 'translate(%f,%f)' % (x,y) ) )
else:
matNew2 = matNew
v = node.get( 'visibility', v )
self.recursivelyTraverseSvg( refnode, matNew2, v, find_bbox )
elif node.tag == inkex.addNS( 'path', 'svg' ):
path_data = node.get( 'd')
if path_data:
self.getPathVertices( path_data, node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
# Manually transform
#
# <rect x="X" y="Y" width="W" height="H"/>
#
# into
#
# <path d="MX,Y lW,0 l0,H l-W,0 z"/>
#
# I.e., explicitly draw three sides of the rectangle and the
# fourth side implicitly
# Create a path with the outline of the rectangle
x = float( node.get( 'x' ) )
y = float( node.get( 'y' ) )
if ( not x ) or ( not y ):
pass
w = float( node.get( 'width', '0' ) )
h = float( node.get( 'height', '0' ) )
a = []
a.append( ['M ', [x, y]] )
a.append( [' l ', [w, 0]] )
a.append( [' l ', [0, h]] )
a.append( [' l ', [-w, 0]] )
a.append( [' Z', []] )
self.getPathVertices( simplepath.formatPath( a ), node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
# Convert
#
# <line x1="X1" y1="Y1" x2="X2" y2="Y2/>
#
# to
#
# <path d="MX1,Y1 LX2,Y2"/>
x1 = float( node.get( 'x1' ) )
y1 = float( node.get( 'y1' ) )
x2 = float( node.get( 'x2' ) )
y2 = float( node.get( 'y2' ) )
if ( not x1 ) or ( not y1 ) or ( not x2 ) or ( not y2 ):
pass
a = []
a.append( ['M ', [x1, y1]] )
a.append( [' L ', [x2, y2]] )
self.getPathVertices( simplepath.formatPath( a ), node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
# Convert
#
# <polyline points="x1,y1 x2,y2 x3,y3 [...]"/>
#
# to
#
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...]"/>
#
# Note: we ignore polylines with no points
pl = node.get( 'points', '' ).strip()
if pl == '':
pass
pa = pl.split()
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
self.getPathVertices( d, node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon':
# Convert
#
# <polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
#
# to
#
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...] Z"/>
#
# Note: we ignore polygons with no points
pl = node.get( 'points', '' ).strip()
if pl == '':
pass
pa = pl.split()
d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] )
d += " Z"
self.getPathVertices( d, node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'ellipse', 'svg' ) or \
node.tag == 'ellipse' or \
node.tag == inkex.addNS( 'circle', 'svg' ) or \
node.tag == 'circle':
# Convert circles and ellipses to a path with two 180 degree arcs.
# In general (an ellipse), we convert
#
# <ellipse rx="RX" ry="RY" cx="X" cy="Y"/>
#
# to
#
# <path d="MX1,CY A RX,RY 0 1 0 X2,CY A RX,RY 0 1 0 X1,CY"/>
#
# where
#
# X1 = CX - RX
# X2 = CX + RX
#
# Note: ellipses or circles with a radius attribute of value 0 are ignored
if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse':
rx = float( node.get( 'rx', '0' ) )
ry = float( node.get( 'ry', '0' ) )
else:
rx = float( node.get( 'r', '0' ) )
ry = rx
if rx == 0 or ry == 0:
pass
cx = float( node.get( 'cx', '0' ) )
cy = float( node.get( 'cy', '0' ) )
x1 = cx - rx
x2 = cx + rx
d = 'M %f,%f ' % ( x1, cy ) + \
'A %f,%f ' % ( rx, ry ) + \
'0 1 0 %f,%f ' % ( x2, cy ) + \
'A %f,%f ' % ( rx, ry ) + \
'0 1 0 %f,%f' % ( x1, cy )
self.mapPathVertices( d, node, matNew, find_bbox )
elif node.tag == inkex.addNS( 'pattern', 'svg' ) or node.tag == 'pattern':
pass
elif node.tag == inkex.addNS( 'metadata', 'svg' ) or node.tag == 'metadata':
pass
elif node.tag == inkex.addNS( 'defs', 'svg' ) or node.tag == 'defs':
pass
elif node.tag == inkex.addNS( 'namedview', 'sodipodi' ) or node.tag == 'namedview':
pass
elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot':
pass
elif node.tag == inkex.addNS( 'text', 'svg' ) or node.tag == 'text':
inkex.errormsg( 'Warning: unable to draw text, please convert it to a path first.' )
pass
elif not isinstance( node.tag, basestring ):
pass
else:
inkex.errormsg( 'Warning: unable to draw object <%s>, please convert it to a path first.' % node.tag )
pass
def recursivelyReplaceSvg( self, nodes, parent_visibility='visible' ):
for i in range( 0, len( nodes ) ):
node = nodes[i]
# Ignore invisible nodes
v = node.get( 'visibility', parent_visibility )
if v == 'inherit':
v = parent_visibility
if v == 'hidden' or v == 'collapse':
pass
if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
self.recursivelyReplaceSvg( node, parent_visibility=v )
elif node.tag == inkex.addNS( 'path', 'svg' ):
if self.paths.has_key( node ):
# Change the path data to be the new path
node.set( 'd', self.paths[node][1:] )
del self.paths[node]
elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use' or \
node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect' or \
node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line' or \
node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline' or \
node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon' or \
node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse' or \
node.tag == inkex.addNS( 'circle', 'svg' ) or node.tag == 'circle':
# Replace this element with a <path> element
if self.paths.has_key( node ):
# Create a new <path> element
# We simply copy all of the attributes from
# the old element to this new element even though
# some of the attributes are no longer relevant
newNode = inkex.etree.Element( inkex.addNS( 'path', 'svg' ), node.attrib )
newNode.set( 'd', self.paths[node][1:] )
# Now replace the old element with this element
nodes[i] = newNode
# And dispose of the old data and element
del self.paths[node]
del node
else:
pass
def recursivelyGetEnclosingTransform( self, node ):
'''
Determine the cumulative transform which node inherits from
its chain of ancestors.
'''
node = node.getparent()
if node is not None:
parent_transform = self.recursivelyGetEnclosingTransform( node )
node_transform = node.get( 'transform', None )
if node_transform is None:
return parent_transform
else:
tr = simpletransform.parseTransform( node_transform )
if parent_transform is None:
return tr
else:
return simpletransform.composeTransform( parent_transform, tr )
else:
return self.docTransform
def effect( self ):
# Viewbox handling
self.handleViewBox()
# Locate the center of the document by obtaining its dimensions
if ( self.docHeight is None ) or (self.docWidth is None ):
inkex.errormsg( 'Document has inappropriate width or height units' )
return
self.cy = self.docHeight / float ( 2 )
self.cx = self.docWidth / float( 2 )
# First traverse the document (or selected items), reducing
# everything to line segments. If working on a selection,
# then determine the selection's bounding box in the process.
# (Actually, we just need to know it's extrema on the x-axis.)
if self.options.ids:
# Traverse the selected objects
for id in self.options.ids:
transform = self.recursivelyGetEnclosingTransform( self.selected[id] )
self.recursivelyTraverseSvg( [self.selected[id]], transform, find_bbox=True )
# Use as the vertical centerline the midpoint between
# the bounding box's extremal X coordinates
self.cx = 0.5 * ( self.xmin + self.xmax )
else:
# Traverse the entire document building new, transformed paths
self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform )
# Now that we know the x-axis extrema, we can remap the data
# Had we know the x-axis extrema in advance (i.e., operating
# on the entire document), then we could have done the mapping
# at the same time we "rendered" everything to line segments.
for key in self.paths:
self.mapPathVertices( key )
# And now replace the old paths with the new paths
# WE DO NOT compute and replace the paths in the same pass!
# So doing can cause multiple transformations of cloned paths
self.recursivelyReplaceSvg( self.document.getroot(), self.docTransform )
if __name__ == '__main__':
e = Map()
e.affect()