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(os.path.join(os.getcwd(),os.path.dirname(sys.argv[0])))
43 tree = inkex.etree.parse( extensionDir + "/" + svgPath )
44 root = tree.getroot()
45 pathElement = root.find('{http://www.w3.org/2000/svg}path')
46 if pathElement == None:
47 return None, 0, 0
48 d = pathElement.get("d")
49 width = float(root.get("width"))
50 height = float(root.get("height"))
51 return simplepath.parsePath(d), width, height # Currently we only support a single path
53 def combinePaths( pathA, pathB ):
54 if pathA == None and pathB == None:
55 return None
56 elif pathA == None:
57 return pathB
58 elif pathB == None:
59 return pathA
60 else:
61 return pathA + pathB
63 def flipLeftRight( sp, width ):
64 for cmd,params in sp:
65 defs = simplepath.pathdefs[cmd]
66 for i in range(defs[1]):
67 if defs[3][i] == 'x':
68 params[i] = width - params[i]
70 def flipTopBottom( sp, height ):
71 for cmd,params in sp:
72 defs = simplepath.pathdefs[cmd]
73 for i in range(defs[1]):
74 if defs[3][i] == 'y':
75 params[i] = height - params[i]
77 def solveQuadratic(a, b, c):
78 det = b*b - 4.0*a*c
79 if det >= 0: # real roots
80 sdet = math.sqrt(det)
81 else: # complex roots
82 sdet = cmath.sqrt(det)
83 return (-b + sdet) / (2*a), (-b - sdet) / (2*a)
85 def cbrt(x):
86 if x >= 0:
87 return x**(1.0/3.0)
88 else:
89 return -((-x)**(1.0/3.0))
91 def findRealRoots(a,b,c,d):
92 if a != 0:
93 a, b, c, d = 1, b/float(a), c/float(a), d/float(a) # Divide through by a
94 t = b / 3.0
95 p, q = c - 3 * t**2, d - c * t + 2 * t**3
96 u, v = solveQuadratic(1, q, -(p/3.0)**3)
97 if type(u) == type(0j): # Complex Cubic Root
98 r = math.sqrt(u.real**2 + u.imag**2)
99 w = math.atan2(u.imag, u.real)
100 y1 = 2 * cbrt(r) * math.cos(w / 3.0)
101 else: # Complex Real Root
102 y1 = cbrt(u) + cbrt(v)
104 y2, y3 = solveQuadratic(1, y1, p + y1**2)
106 if type(y2) == type(0j): # Are y2 and y3 complex?
107 return [y1 - t]
108 return [y1 - t, y2 - t, y3 - t]
109 elif b != 0:
110 det=c*c - 4.0*b*d
111 if det >= 0:
112 return [(-c + math.sqrt(det))/(2.0*b),(-c - math.sqrt(det))/(2.0*b)]
113 elif c != 0:
114 return [-d/c]
115 return []
117 def getPathBoundingBox( sp ):
119 box = None
120 last = None
121 lostctrl = None
123 for cmd,params in sp:
125 segmentBox = None
127 if cmd == 'M':
128 # A move cannot contribute to the bounding box
129 last = params[:]
130 lastctrl = params[:]
131 elif cmd == 'L':
132 if last:
133 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
134 last = params[:]
135 lastctrl = params[:]
136 elif cmd == 'C':
137 if last:
138 segmentBox = (min(params[4], last[0]), max(params[4], last[0]), min(params[5], last[1]), max(params[5], last[1]))
140 bx0, by0 = last[:]
141 bx1, by1, bx2, by2, bx3, by3 = params[:]
143 # Compute the x limits
144 a = (-bx0 + 3*bx1 - 3*bx2 + bx3)*3
145 b = (3*bx0 - 6*bx1 + 3*bx2)*2
146 c = (-3*bx0 + 3*bx1)
147 ts = findRealRoots(0, a, b, c)
148 for t in ts:
149 if t >= 0 and t <= 1:
150 x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \
151 (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \
152 (-3*bx0 + 3*bx1)*t + \
153 bx0
154 segmentBox = (min(segmentBox[0], x), max(segmentBox[1], x), segmentBox[2], segmentBox[3])
156 # Compute the y limits
157 a = (-by0 + 3*by1 - 3*by2 + by3)*3
158 b = (3*by0 - 6*by1 + 3*by2)*2
159 c = (-3*by0 + 3*by1)
160 ts = findRealRoots(0, a, b, c)
161 for t in ts:
162 if t >= 0 and t <= 1:
163 y = (-by0 + 3*by1 - 3*by2 + by3)*(t**3) + \
164 (3*by0 - 6*by1 + 3*by2)*(t**2) + \
165 (-3*by0 + 3*by1)*t + \
166 by0
167 segmentBox = (segmentBox[0], segmentBox[1], min(segmentBox[2], y), max(segmentBox[3], y))
169 last = params[-2:]
170 lastctrl = params[2:4]
172 elif cmd == 'Q':
173 # Provisional
174 if last:
175 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
176 last = params[-2:]
177 lastctrl = params[2:4]
179 elif cmd == 'A':
180 # Provisional
181 if last:
182 segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1]))
183 last = params[-2:]
184 lastctrl = params[2:4]
186 if segmentBox:
187 if box:
188 box = (min(segmentBox[0],box[0]), max(segmentBox[1],box[1]), min(segmentBox[2],box[2]), max(segmentBox[3],box[3]))
189 else:
190 box = segmentBox
191 return box
193 def mxfm( image, width, height, stack ): # returns possibly transformed image
194 tbimage = image
195 if ( stack[0] == "-" ): # top-bottom flip
196 flipTopBottom(tbimage, height)
197 stack.pop( 0 )
199 lrimage = tbimage
200 if ( stack[0] == "|" ): # left-right flip
201 flipLeftRight(tbimage, width)
202 stack.pop( 0 )
203 return lrimage
205 def comparerule( rule, nodes ): # compare node list to nodes in rule
206 for i in range( 0, len(nodes)): # range( a, b ) = (a, a+1, a+2 ... b-2, b-1)
207 if (nodes[i] == rule[i][0]):
208 pass
209 else: return 0
210 return 1
212 def findrule( state, nodes ): # find the rule which generated this subtree
213 ruleset = syntax[state][1]
214 nodelen = len(nodes)
215 for rule in ruleset:
216 rulelen = len(rule)
217 if ((rulelen == nodelen) and (comparerule( rule, nodes ))):
218 return rule
219 return
221 def generate( state ): # generate a random tree (in stack form)
222 stack = [ state ]
223 if ( len(syntax[state]) == 1 ): # if this is a stop symbol
224 return stack
225 else:
226 stack.append( "[" )
227 path = random.randint(0, (len(syntax[state][1])-1)) # choose randomly from next states
228 for symbol in syntax[state][1][path]: # recurse down each non-terminal
229 if ( symbol != 0 ): # 0 denotes end of list ###
230 substack = generate( symbol[0] ) # get subtree
231 for elt in substack:
232 stack.append( elt )
233 if (symbol[3]):stack.append( "-" ) # top-bottom flip
234 if (symbol[4]):stack.append( "|" ) # left-right flip
235 #else:
236 #inkex.debug("found end of list in generate( state =", state, ")") # this should be deprecated/never happen
237 stack.append("]")
238 return stack
240 def draw( stack ): # draw a character based on a tree stack
241 state = stack.pop(0)
242 #print state,
244 image, width, height = loadPath( font+syntax[state][0] ) # load the image
245 if (stack[0] != "["): # terminal stack element
246 if (len(syntax[state]) == 1): # this state is a terminal node
247 return image, width, height
248 else:
249 substack = generate( state ) # generate random substack
250 return draw( substack ) # draw random substack
251 else:
252 #inkex.debug("[")
253 stack.pop(0)
254 images = [] # list of daughter images
255 nodes = [] # list of daughter names
256 while (stack[0] != "]"): # for all nodes in stack
257 newstate = stack[0] # the new state
258 newimage, width, height = draw( stack ) # draw the daughter state
259 if (newimage):
260 tfimage = mxfm( newimage, width, height, stack ) # maybe transform daughter state
261 images.append( [tfimage, width, height] ) # list of daughter images
262 nodes.append( newstate ) # list of daughter nodes
263 else:
264 #inkex.debug(("recurse on",newstate,"failed")) # this should never happen
265 return None, 0, 0
266 rule = findrule( state, nodes ) # find the rule for this subtree
268 for i in range( 0, len(images)):
269 currimg, width, height = images[i]
271 if currimg:
272 #box = getPathBoundingBox(currimg)
273 dx = rule[i][1]*units
274 dy = rule[i][2]*units
275 #newbox = ((box[0]+dx),(box[1]+dy),(box[2]+dx),(box[3]+dy))
276 simplepath.translatePath(currimg, dx, dy)
277 image = combinePaths( image, currimg )
279 stack.pop( 0 )
280 return image, width, height
282 def draw_crop_scale( stack, zoom ): # draw, crop and scale letter image
283 image, width, height = draw(stack)
284 bbox = getPathBoundingBox(image)
285 simplepath.translatePath(image, -bbox[0], 0)
286 simplepath.scalePath(image, zoom/units, zoom/units)
287 return image, bbox[1] - bbox[0], bbox[3] - bbox[2]
289 def randomize_input_string( str, zoom ): # generate list of images based on input string
290 imagelist = []
292 for i in range(0,len(str)):
293 char = str[i]
294 #if ( re.match("[a-zA-Z0-9?]", char)):
295 if ( alphabet.has_key(char)):
296 if ((i > 0) and (char == str[i-1])): # if this letter matches previous letter
297 imagelist.append(imagelist[len(stack)-1])# make them the same image
298 else: # generate image for letter
299 stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-1))] , "." )
300 #stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-2))] , "." )
301 imagelist.append( draw_crop_scale( stack, zoom ))
302 elif( char == " "): # add a " " space to the image list
303 imagelist.append( " " )
304 else: # this character is not in config.alphabet, skip it
305 print "bad character", char
306 return imagelist
308 def optikern( image, width, zoom ): # optical kerning algorithm
309 left = []
310 right = []
312 for i in range( 0, 36 ):
313 y = 0.5 * (i + 0.5) * zoom
314 xmin = None
315 xmax = None
317 for cmd,params in image:
319 segmentBox = None
321 if cmd == 'M':
322 # A move cannot contribute to the bounding box
323 last = params[:]
324 lastctrl = params[:]
325 elif cmd == 'L':
326 if (y >= last[1] and y <= params[1]) or (y >= params[1] and y <= last[1]):
327 if params[0] == last[0]:
328 x = params[0]
329 else:
330 a = (params[1] - last[1]) / (params[0] - last[0])
331 b = last[1] - a * last[0]
332 if a != 0:
333 x = (y - b) / a
334 else: x = None
336 if x:
337 if xmin == None or x < xmin: xmin = x
338 if xmax == None or x > xmax: xmax = x
340 last = params[:]
341 lastctrl = params[:]
342 elif cmd == 'C':
343 if last:
344 bx0, by0 = last[:]
345 bx1, by1, bx2, by2, bx3, by3 = params[:]
347 d = by0 - y
348 c = -3*by0 + 3*by1
349 b = 3*by0 - 6*by1 + 3*by2
350 a = -by0 + 3*by1 - 3*by2 + by3
352 ts = findRealRoots(a, b, c, d)
354 for t in ts:
355 if t >= 0 and t <= 1:
356 x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \
357 (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \
358 (-3*bx0 + 3*bx1)*t + \
359 bx0
360 if xmin == None or x < xmin: xmin = x
361 if xmax == None or x > xmax: xmax = x
363 last = params[-2:]
364 lastctrl = params[2:4]
366 elif cmd == 'Q':
367 # Quadratic beziers are ignored
368 last = params[-2:]
369 lastctrl = params[2:4]
371 elif cmd == 'A':
372 # Arcs are ignored
373 last = params[-2:]
374 lastctrl = params[2:4]
377 if xmin != None and xmax != None:
378 left.append( xmin ) # distance from left edge of region to left edge of bbox
379 right.append( width - xmax ) # distance from right edge of region to right edge of bbox
380 else:
381 left.append( width )
382 right.append( width )
384 return (left, right)
386 def layoutstring( imagelist, zoom ): # layout string of letter-images using optical kerning
387 kernlist = []
388 length = zoom
389 for entry in imagelist:
390 if (entry == " "): # leaving room for " " space characters
391 length = length + (zoom * render_alphabetsoup_config.space)
392 else:
393 image, width, height = entry
394 length = length + width + zoom # add letter length to overall length
395 kernlist.append( optikern(image, width, zoom) ) # append kerning data for this image
397 workspace = None
399 position = zoom
400 for i in range(0, len(kernlist)):
401 while(imagelist[i] == " "):
402 position = position + (zoom * render_alphabetsoup_config.space )
403 imagelist.pop(i)
404 image, width, height = imagelist[i]
406 # set the kerning
407 if i == 0: kern = 0 # for first image, kerning is zero
408 else:
409 kerncompare = [] # kerning comparison array
410 for j in range( 0, len(kernlist[i][0])):
411 kerncompare.append( kernlist[i][0][j]+kernlist[i-1][1][j] )
412 kern = min( kerncompare )
414 position = position - kern # move position back by kern amount
415 thisimage = copy.deepcopy(image)
416 simplepath.translatePath(thisimage, position, 0)
417 workspace = combinePaths(workspace, thisimage)
418 position = position + width + zoom # advance position by letter width
420 return workspace
422 class AlphabetSoup(inkex.Effect):
423 def __init__(self):
424 inkex.Effect.__init__(self)
425 self.OptionParser.add_option("-t", "--text",
426 action="store", type="string",
427 dest="text", default="Inkscape",
428 help="The text for alphabet soup")
429 self.OptionParser.add_option("-z", "--zoom",
430 action="store", type="float",
431 dest="zoom", default="8.0",
432 help="The zoom on the output graphics")
433 self.OptionParser.add_option("-s", "--seed",
434 action="store", type="int",
435 dest="seed", default="0",
436 help="The random seed for the soup")
438 def effect(self):
439 zoom = self.options.zoom
440 random.seed(self.options.seed)
442 imagelist = randomize_input_string(self.options.text, zoom)
443 image = layoutstring( imagelist, zoom )
445 if image:
446 s = { 'stroke': 'none', 'fill': '#000000' }
448 new = inkex.etree.Element(inkex.addNS('path','svg'))
449 new.set('style', simplestyle.formatStyle(s))
451 new.set('d', simplepath.formatPath(image))
452 self.current_layer.append(new)
454 e = AlphabetSoup()
455 e.affect()