Code

patch by Adib for 406470
[inkscape.git] / share / extensions / render_alphabetsoup.py
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)
107                 
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 ):
122         
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]))
143                                 
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
339                                         
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
355                                         
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()