1 #!/usr/bin/env python
2 '''
3 Copyright (C) 2001-2002 Matt Chisholm matt@theory.org
4 Copyright (C) 2008 Joel Holdsworth joel@airwebreathe.org.uk
5 for AP
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 '''
22 import copy
23 import inkex
24 import simplestyle
25 import math
26 import cmath
27 import string
28 import random
29 import render_alphabetsoup_config
30 import bezmisc
31 import simplepath
32 import os
33 import sys
35 syntax = render_alphabetsoup_config.syntax
36 alphabet = render_alphabetsoup_config.alphabet
37 units = render_alphabetsoup_config.units
38 font = render_alphabetsoup_config.font
40 # Loads a super-path from a given SVG file
41 def loadPath( svgPath ):
42 extensionDir = os.path.normpath(
43 os.path.join( os.getcwd(), os.path.dirname(__file__) )
44 )
45 # __file__ is better then sys.argv[0] because this file may be a module
46 # for another one.
47 tree = inkex.etree.parse( extensionDir + "/" + svgPath )
48 root = tree.getroot()
49 pathElement = root.find('{http://www.w3.org/2000/svg}path')
50 if pathElement == None:
51 return None, 0, 0
52 d = pathElement.get("d")
53 width = float(root.get("width"))
54 height = float(root.get("height"))
55 return simplepath.parsePath(d), width, height # Currently we only support a single path
57 def combinePaths( pathA, pathB ):
58 if pathA == None and pathB == None:
59 return None
60 elif pathA == None:
61 return pathB
62 elif pathB == None:
63 return pathA
64 else:
65 return pathA + pathB
67 def flipLeftRight( sp, width ):
68 for cmd,params in sp:
69 defs = simplepath.pathdefs[cmd]
70 for i in range(defs[1]):
71 if defs[3][i] == 'x':
72 params[i] = width - params[i]
74 def flipTopBottom( sp, height ):
75 for cmd,params in sp:
76 defs = simplepath.pathdefs[cmd]
77 for i in range(defs[1]):
78 if defs[3][i] == 'y':
79 params[i] = height - params[i]
81 def solveQuadratic(a, b, c):
82 det = b*b - 4.0*a*c
83 if det >= 0: # real roots
84 sdet = math.sqrt(det)
85 else: # complex roots
86 sdet = cmath.sqrt(det)
87 return (-b + sdet) / (2*a), (-b - sdet) / (2*a)
89 def cbrt(x):
90 if x >= 0:
91 return x**(1.0/3.0)
92 else:
93 return -((-x)**(1.0/3.0))
95 def findRealRoots(a,b,c,d):
96 if a != 0:
97 a, b, c, d = 1, b/float(a), c/float(a), d/float(a) # Divide through by a
98 t = b / 3.0
99 p, q = c - 3 * t**2, d - c * t + 2 * t**3
100 u, v = solveQuadratic(1, q, -(p/3.0)**3)
101 if type(u) == type(0j): # Complex Cubic Root
102 r = math.sqrt(u.real**2 + u.imag**2)
103 w = math.atan2(u.imag, u.real)
104 y1 = 2 * cbrt(r) * math.cos(w / 3.0)
105 else: # Complex Real Root
106 y1 = cbrt(u) + cbrt(v)
108 y2, y3 = solveQuadratic(1, y1, p + y1**2)
110 if type(y2) == type(0j): # Are y2 and y3 complex?
111 return [y1 - t]
112 return [y1 - t, y2 - t, y3 - t]
113 elif b != 0:
114 det=c*c - 4.0*b*d
115 if det >= 0:
116 return [(-c + math.sqrt(det))/(2.0*b),(-c - math.sqrt(det))/(2.0*b)]
117 elif c != 0:
118 return [-d/c]
119 return []
121 def getPathBoundingBox( sp ):
123 box = None
124 last = None
125 lostctrl = None
127 for cmd,params in sp:
129 segmentBox = None
131 if cmd == 'M':
132 # A move cannot contribute to the bounding box
133 last = params[:]
134 lastctrl = params[:]
135 elif cmd == 'L':
136 if last:
137 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
138 last = params[:]
139 lastctrl = params[:]
140 elif cmd == 'C':
141 if last:
142 segmentBox = (min(params[4], last[0]), max(params[4], last[0]), min(params[5], last[1]), max(params[5], last[1]))
144 bx0, by0 = last[:]
145 bx1, by1, bx2, by2, bx3, by3 = params[:]
147 # Compute the x limits
148 a = (-bx0 + 3*bx1 - 3*bx2 + bx3)*3
149 b = (3*bx0 - 6*bx1 + 3*bx2)*2
150 c = (-3*bx0 + 3*bx1)
151 ts = findRealRoots(0, a, b, c)
152 for t in ts:
153 if t >= 0 and t <= 1:
154 x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \
155 (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \
156 (-3*bx0 + 3*bx1)*t + \
157 bx0
158 segmentBox = (min(segmentBox[0], x), max(segmentBox[1], x), segmentBox[2], segmentBox[3])
160 # Compute the y limits
161 a = (-by0 + 3*by1 - 3*by2 + by3)*3
162 b = (3*by0 - 6*by1 + 3*by2)*2
163 c = (-3*by0 + 3*by1)
164 ts = findRealRoots(0, a, b, c)
165 for t in ts:
166 if t >= 0 and t <= 1:
167 y = (-by0 + 3*by1 - 3*by2 + by3)*(t**3) + \
168 (3*by0 - 6*by1 + 3*by2)*(t**2) + \
169 (-3*by0 + 3*by1)*t + \
170 by0
171 segmentBox = (segmentBox[0], segmentBox[1], min(segmentBox[2], y), max(segmentBox[3], y))
173 last = params[-2:]
174 lastctrl = params[2:4]
176 elif cmd == 'Q':
177 # Provisional
178 if last:
179 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
180 last = params[-2:]
181 lastctrl = params[2:4]
183 elif cmd == 'A':
184 # Provisional
185 if last:
186 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
187 last = params[-2:]
188 lastctrl = params[2:4]
190 if segmentBox:
191 if box:
192 box = (min(segmentBox[0],box[0]), max(segmentBox[1],box[1]), min(segmentBox[2],box[2]), max(segmentBox[3],box[3]))
193 else:
194 box = segmentBox
195 return box
197 def mxfm( image, width, height, stack ): # returns possibly transformed image
198 tbimage = image
199 if ( stack[0] == "-" ): # top-bottom flip
200 flipTopBottom(tbimage, height)
201 stack.pop( 0 )
203 lrimage = tbimage
204 if ( stack[0] == "|" ): # left-right flip
205 flipLeftRight(tbimage, width)
206 stack.pop( 0 )
207 return lrimage
209 def comparerule( rule, nodes ): # compare node list to nodes in rule
210 for i in range( 0, len(nodes)): # range( a, b ) = (a, a+1, a+2 ... b-2, b-1)
211 if (nodes[i] == rule[i][0]):
212 pass
213 else: return 0
214 return 1
216 def findrule( state, nodes ): # find the rule which generated this subtree
217 ruleset = syntax[state][1]
218 nodelen = len(nodes)
219 for rule in ruleset:
220 rulelen = len(rule)
221 if ((rulelen == nodelen) and (comparerule( rule, nodes ))):
222 return rule
223 return
225 def generate( state ): # generate a random tree (in stack form)
226 stack = [ state ]
227 if ( len(syntax[state]) == 1 ): # if this is a stop symbol
228 return stack
229 else:
230 stack.append( "[" )
231 path = random.randint(0, (len(syntax[state][1])-1)) # choose randomly from next states
232 for symbol in syntax[state][1][path]: # recurse down each non-terminal
233 if ( symbol != 0 ): # 0 denotes end of list ###
234 substack = generate( symbol[0] ) # get subtree
235 for elt in substack:
236 stack.append( elt )
237 if (symbol[3]):stack.append( "-" ) # top-bottom flip
238 if (symbol[4]):stack.append( "|" ) # left-right flip
239 #else:
240 #inkex.debug("found end of list in generate( state =", state, ")") # this should be deprecated/never happen
241 stack.append("]")
242 return stack
244 def draw( stack ): # draw a character based on a tree stack
245 state = stack.pop(0)
246 #print state,
248 image, width, height = loadPath( font+syntax[state][0] ) # load the image
249 if (stack[0] != "["): # terminal stack element
250 if (len(syntax[state]) == 1): # this state is a terminal node
251 return image, width, height
252 else:
253 substack = generate( state ) # generate random substack
254 return draw( substack ) # draw random substack
255 else:
256 #inkex.debug("[")
257 stack.pop(0)
258 images = [] # list of daughter images
259 nodes = [] # list of daughter names
260 while (stack[0] != "]"): # for all nodes in stack
261 newstate = stack[0] # the new state
262 newimage, width, height = draw( stack ) # draw the daughter state
263 if (newimage):
264 tfimage = mxfm( newimage, width, height, stack ) # maybe transform daughter state
265 images.append( [tfimage, width, height] ) # list of daughter images
266 nodes.append( newstate ) # list of daughter nodes
267 else:
268 #inkex.debug(("recurse on",newstate,"failed")) # this should never happen
269 return None, 0, 0
270 rule = findrule( state, nodes ) # find the rule for this subtree
272 for i in range( 0, len(images)):
273 currimg, width, height = images[i]
275 if currimg:
276 #box = getPathBoundingBox(currimg)
277 dx = rule[i][1]*units
278 dy = rule[i][2]*units
279 #newbox = ((box[0]+dx),(box[1]+dy),(box[2]+dx),(box[3]+dy))
280 simplepath.translatePath(currimg, dx, dy)
281 image = combinePaths( image, currimg )
283 stack.pop( 0 )
284 return image, width, height
286 def draw_crop_scale( stack, zoom ): # draw, crop and scale letter image
287 image, width, height = draw(stack)
288 bbox = getPathBoundingBox(image)
289 simplepath.translatePath(image, -bbox[0], 0)
290 simplepath.scalePath(image, zoom/units, zoom/units)
291 return image, bbox[1] - bbox[0], bbox[3] - bbox[2]
293 def randomize_input_string( str, zoom ): # generate list of images based on input string
294 imagelist = []
296 for i in range(0,len(str)):
297 char = str[i]
298 #if ( re.match("[a-zA-Z0-9?]", char)):
299 if ( alphabet.has_key(char)):
300 if ((i > 0) and (char == str[i-1])): # if this letter matches previous letter
301 imagelist.append(imagelist[len(stack)-1])# make them the same image
302 else: # generate image for letter
303 stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-1))] , "." )
304 #stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-2))] , "." )
305 imagelist.append( draw_crop_scale( stack, zoom ))
306 elif( char == " "): # add a " " space to the image list
307 imagelist.append( " " )
308 else: # this character is not in config.alphabet, skip it
309 print "bad character", char
310 return imagelist
312 def optikern( image, width, zoom ): # optical kerning algorithm
313 left = []
314 right = []
316 for i in range( 0, 36 ):
317 y = 0.5 * (i + 0.5) * zoom
318 xmin = None
319 xmax = None
321 for cmd,params in image:
323 segmentBox = None
325 if cmd == 'M':
326 # A move cannot contribute to the bounding box
327 last = params[:]
328 lastctrl = params[:]
329 elif cmd == 'L':
330 if (y >= last[1] and y <= params[1]) or (y >= params[1] and y <= last[1]):
331 if params[0] == last[0]:
332 x = params[0]
333 else:
334 a = (params[1] - last[1]) / (params[0] - last[0])
335 b = last[1] - a * last[0]
336 if a != 0:
337 x = (y - b) / a
338 else: x = None
340 if x:
341 if xmin == None or x < xmin: xmin = x
342 if xmax == None or x > xmax: xmax = x
344 last = params[:]
345 lastctrl = params[:]
346 elif cmd == 'C':
347 if last:
348 bx0, by0 = last[:]
349 bx1, by1, bx2, by2, bx3, by3 = params[:]
351 d = by0 - y
352 c = -3*by0 + 3*by1
353 b = 3*by0 - 6*by1 + 3*by2
354 a = -by0 + 3*by1 - 3*by2 + by3
356 ts = findRealRoots(a, b, c, d)
358 for t in ts:
359 if t >= 0 and t <= 1:
360 x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \
361 (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \
362 (-3*bx0 + 3*bx1)*t + \
363 bx0
364 if xmin == None or x < xmin: xmin = x
365 if xmax == None or x > xmax: xmax = x
367 last = params[-2:]
368 lastctrl = params[2:4]
370 elif cmd == 'Q':
371 # Quadratic beziers are ignored
372 last = params[-2:]
373 lastctrl = params[2:4]
375 elif cmd == 'A':
376 # Arcs are ignored
377 last = params[-2:]
378 lastctrl = params[2:4]
381 if xmin != None and xmax != None:
382 left.append( xmin ) # distance from left edge of region to left edge of bbox
383 right.append( width - xmax ) # distance from right edge of region to right edge of bbox
384 else:
385 left.append( width )
386 right.append( width )
388 return (left, right)
390 def layoutstring( imagelist, zoom ): # layout string of letter-images using optical kerning
391 kernlist = []
392 length = zoom
393 for entry in imagelist:
394 if (entry == " "): # leaving room for " " space characters
395 length = length + (zoom * render_alphabetsoup_config.space)
396 else:
397 image, width, height = entry
398 length = length + width + zoom # add letter length to overall length
399 kernlist.append( optikern(image, width, zoom) ) # append kerning data for this image
401 workspace = None
403 position = zoom
404 for i in range(0, len(kernlist)):
405 while(imagelist[i] == " "):
406 position = position + (zoom * render_alphabetsoup_config.space )
407 imagelist.pop(i)
408 image, width, height = imagelist[i]
410 # set the kerning
411 if i == 0: kern = 0 # for first image, kerning is zero
412 else:
413 kerncompare = [] # kerning comparison array
414 for j in range( 0, len(kernlist[i][0])):
415 kerncompare.append( kernlist[i][0][j]+kernlist[i-1][1][j] )
416 kern = min( kerncompare )
418 position = position - kern # move position back by kern amount
419 thisimage = copy.deepcopy(image)
420 simplepath.translatePath(thisimage, position, 0)
421 workspace = combinePaths(workspace, thisimage)
422 position = position + width + zoom # advance position by letter width
424 return workspace
426 class AlphabetSoup(inkex.Effect):
427 def __init__(self):
428 inkex.Effect.__init__(self)
429 self.OptionParser.add_option("-t", "--text",
430 action="store", type="string",
431 dest="text", default="Inkscape",
432 help="The text for alphabet soup")
433 self.OptionParser.add_option("-z", "--zoom",
434 action="store", type="float",
435 dest="zoom", default="8.0",
436 help="The zoom on the output graphics")
437 self.OptionParser.add_option("-s", "--seed",
438 action="store", type="int",
439 dest="seed", default="0",
440 help="The random seed for the soup")
442 def effect(self):
443 zoom = self.options.zoom
444 random.seed(self.options.seed)
446 imagelist = randomize_input_string(self.options.text, zoom)
447 image = layoutstring( imagelist, zoom )
449 if image:
450 s = { 'stroke': 'none', 'fill': '#000000' }
452 new = inkex.etree.Element(inkex.addNS('path','svg'))
453 new.set('style', simplestyle.formatStyle(s))
455 new.set('d', simplepath.formatPath(image))
456 self.current_layer.append(new)
458 if __name__ == '__main__':
459 e = AlphabetSoup()
460 e.affect()