Code

Several different i18n issues fixed following report from a_b (adresses bug #215387...
[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(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)
103                 
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 ):
118         
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]))
139                                 
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
335                                         
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
351                                         
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()