#!/usr/bin/env python ''' Copyright (C) 2001-2002 Matt Chisholm matt@theory.org Copyright (C) 2008 Joel Holdsworth joel@airwebreathe.org.uk for AP 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 ''' # standard library import copy import math import cmath import string import random import os import sys import re # local library import inkex import simplestyle import render_alphabetsoup_config import bezmisc import simplepath inkex.localize() syntax = render_alphabetsoup_config.syntax alphabet = render_alphabetsoup_config.alphabet units = render_alphabetsoup_config.units font = render_alphabetsoup_config.font # Loads a super-path from a given SVG file def loadPath( svgPath ): extensionDir = os.path.normpath( os.path.join( os.getcwd(), os.path.dirname(__file__) ) ) # __file__ is better then sys.argv[0] because this file may be a module # for another one. tree = inkex.etree.parse( extensionDir + "/" + svgPath ) root = tree.getroot() pathElement = root.find('{http://www.w3.org/2000/svg}path') if pathElement == None: return None, 0, 0 d = pathElement.get("d") width = float(root.get("width")) height = float(root.get("height")) return simplepath.parsePath(d), width, height # Currently we only support a single path def combinePaths( pathA, pathB ): if pathA == None and pathB == None: return None elif pathA == None: return pathB elif pathB == None: return pathA else: return pathA + pathB def reverseComponent(c): nc = [] last = c.pop() nc.append(['M', last[1][-2:]]) while c: this = c.pop() cmd = last[0] if cmd == 'C': nc.append([last[0], last[1][2:4] + last[1][:2] + this[1][-2:]]) else: nc.append([last[0], this[1][-2:]]) last = this return nc def reversePath(sp): rp = [] component = [] for p in sp: cmd, params = p if cmd == 'Z': rp.extend(reverseComponent(component)) rp.append(['Z', []]) component = [] else: component.append(p) return rp def flipLeftRight( sp, width ): for cmd,params in sp: defs = simplepath.pathdefs[cmd] for i in range(defs[1]): if defs[3][i] == 'x': params[i] = width - params[i] def flipTopBottom( sp, height ): for cmd,params in sp: defs = simplepath.pathdefs[cmd] for i in range(defs[1]): if defs[3][i] == 'y': params[i] = height - params[i] def solveQuadratic(a, b, c): det = b*b - 4.0*a*c if det >= 0: # real roots sdet = math.sqrt(det) else: # complex roots sdet = cmath.sqrt(det) return (-b + sdet) / (2*a), (-b - sdet) / (2*a) def cbrt(x): if x >= 0: return x**(1.0/3.0) else: return -((-x)**(1.0/3.0)) def findRealRoots(a,b,c,d): if a != 0: a, b, c, d = 1, b/float(a), c/float(a), d/float(a) # Divide through by a t = b / 3.0 p, q = c - 3 * t**2, d - c * t + 2 * t**3 u, v = solveQuadratic(1, q, -(p/3.0)**3) if type(u) == type(0j): # Complex Cubic Root r = math.sqrt(u.real**2 + u.imag**2) w = math.atan2(u.imag, u.real) y1 = 2 * cbrt(r) * math.cos(w / 3.0) else: # Complex Real Root y1 = cbrt(u) + cbrt(v) y2, y3 = solveQuadratic(1, y1, p + y1**2) if type(y2) == type(0j): # Are y2 and y3 complex? return [y1 - t] return [y1 - t, y2 - t, y3 - t] elif b != 0: det=c*c - 4.0*b*d if det >= 0: return [(-c + math.sqrt(det))/(2.0*b),(-c - math.sqrt(det))/(2.0*b)] elif c != 0: return [-d/c] return [] def getPathBoundingBox( sp ): box = None last = None lostctrl = None for cmd,params in sp: segmentBox = None if cmd == 'M': # A move cannot contribute to the bounding box last = params[:] lastctrl = params[:] elif cmd == 'L': if last: segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1])) last = params[:] lastctrl = params[:] elif cmd == 'C': if last: segmentBox = (min(params[4], last[0]), max(params[4], last[0]), min(params[5], last[1]), max(params[5], last[1])) bx0, by0 = last[:] bx1, by1, bx2, by2, bx3, by3 = params[:] # Compute the x limits a = (-bx0 + 3*bx1 - 3*bx2 + bx3)*3 b = (3*bx0 - 6*bx1 + 3*bx2)*2 c = (-3*bx0 + 3*bx1) ts = findRealRoots(0, a, b, c) for t in ts: if t >= 0 and t <= 1: x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \ (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \ (-3*bx0 + 3*bx1)*t + \ bx0 segmentBox = (min(segmentBox[0], x), max(segmentBox[1], x), segmentBox[2], segmentBox[3]) # Compute the y limits a = (-by0 + 3*by1 - 3*by2 + by3)*3 b = (3*by0 - 6*by1 + 3*by2)*2 c = (-3*by0 + 3*by1) ts = findRealRoots(0, a, b, c) for t in ts: if t >= 0 and t <= 1: y = (-by0 + 3*by1 - 3*by2 + by3)*(t**3) + \ (3*by0 - 6*by1 + 3*by2)*(t**2) + \ (-3*by0 + 3*by1)*t + \ by0 segmentBox = (segmentBox[0], segmentBox[1], min(segmentBox[2], y), max(segmentBox[3], y)) last = params[-2:] lastctrl = params[2:4] elif cmd == 'Q': # Provisional if last: segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1])) last = params[-2:] lastctrl = params[2:4] elif cmd == 'A': # Provisional if last: segmentBox = (min(params[0], last[0]), max(params[0], last[0]), min(params[1], last[1]), max(params[1], last[1])) last = params[-2:] lastctrl = params[2:4] if segmentBox: if box: box = (min(segmentBox[0],box[0]), max(segmentBox[1],box[1]), min(segmentBox[2],box[2]), max(segmentBox[3],box[3])) else: box = segmentBox return box def mxfm( image, width, height, stack ): # returns possibly transformed image tbimage = image if ( stack[0] == "-" ): # top-bottom flip flipTopBottom(tbimage, height) tbimage = reversePath(tbimage) stack.pop( 0 ) lrimage = tbimage if ( stack[0] == "|" ): # left-right flip flipLeftRight(tbimage, width) lrimage = reversePath(lrimage) stack.pop( 0 ) return lrimage def comparerule( rule, nodes ): # compare node list to nodes in rule for i in range( 0, len(nodes)): # range( a, b ) = (a, a+1, a+2 ... b-2, b-1) if (nodes[i] == rule[i][0]): pass else: return 0 return 1 def findrule( state, nodes ): # find the rule which generated this subtree ruleset = syntax[state][1] nodelen = len(nodes) for rule in ruleset: rulelen = len(rule) if ((rulelen == nodelen) and (comparerule( rule, nodes ))): return rule return def generate( state ): # generate a random tree (in stack form) stack = [ state ] if ( len(syntax[state]) == 1 ): # if this is a stop symbol return stack else: stack.append( "[" ) path = random.randint(0, (len(syntax[state][1])-1)) # choose randomly from next states for symbol in syntax[state][1][path]: # recurse down each non-terminal if ( symbol != 0 ): # 0 denotes end of list ### substack = generate( symbol[0] ) # get subtree for elt in substack: stack.append( elt ) if (symbol[3]):stack.append( "-" ) # top-bottom flip if (symbol[4]):stack.append( "|" ) # left-right flip #else: #inkex.debug("found end of list in generate( state =", state, ")") # this should be deprecated/never happen stack.append("]") return stack def draw( stack ): # draw a character based on a tree stack state = stack.pop(0) #print state, image, width, height = loadPath( font+syntax[state][0] ) # load the image if (stack[0] != "["): # terminal stack element if (len(syntax[state]) == 1): # this state is a terminal node return image, width, height else: substack = generate( state ) # generate random substack return draw( substack ) # draw random substack else: #inkex.debug("[") stack.pop(0) images = [] # list of daughter images nodes = [] # list of daughter names while (stack[0] != "]"): # for all nodes in stack newstate = stack[0] # the new state newimage, width, height = draw( stack ) # draw the daughter state if (newimage): tfimage = mxfm( newimage, width, height, stack ) # maybe transform daughter state images.append( [tfimage, width, height] ) # list of daughter images nodes.append( newstate ) # list of daughter nodes else: #inkex.debug(("recurse on",newstate,"failed")) # this should never happen return None, 0, 0 rule = findrule( state, nodes ) # find the rule for this subtree for i in range( 0, len(images)): currimg, width, height = images[i] if currimg: #box = getPathBoundingBox(currimg) dx = rule[i][1]*units dy = rule[i][2]*units #newbox = ((box[0]+dx),(box[1]+dy),(box[2]+dx),(box[3]+dy)) simplepath.translatePath(currimg, dx, dy) image = combinePaths( image, currimg ) stack.pop( 0 ) return image, width, height def draw_crop_scale( stack, zoom ): # draw, crop and scale letter image image, width, height = draw(stack) bbox = getPathBoundingBox(image) simplepath.translatePath(image, -bbox[0], 0) simplepath.scalePath(image, zoom/units, zoom/units) return image, bbox[1] - bbox[0], bbox[3] - bbox[2] def randomize_input_string(tokens, zoom ): # generate a glyph starting from each token in the input string imagelist = [] for i in range(0,len(tokens)): char = tokens[i] #if ( re.match("[a-zA-Z0-9?]", char)): if ( alphabet.has_key(char)): if ((i > 0) and (char == tokens[i-1])): # if this letter matches previous letter imagelist.append(imagelist[len(stack)-1])# make them the same image else: # generate image for letter stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-1))] , "." ) #stack = string.split( alphabet[char][random.randint(0,(len(alphabet[char])-2))] , "." ) imagelist.append( draw_crop_scale( stack, zoom )) elif( char == " "): # add a " " space to the image list imagelist.append( " " ) else: # this character is not in config.alphabet, skip it sys.stderr.write('bad character "%s"\n' % char) return imagelist def generate_random_string( tokens, zoom ): # generate a totally random glyph for each glyph in the input string imagelist = [] for char in tokens: if ( char == " "): # add a " " space to the image list imagelist.append( " " ) else: if ( re.match("[a-z]", char )): # generate lowercase letter stack = generate("lc") elif ( re.match("[A-Z]", char )): # generate uppercase letter stack = generate("UC") else: # this character is not in config.alphabet, skip it sys.stderr.write('bad character"%s"\n' % char) stack = generate("start") imagelist.append( draw_crop_scale( stack, zoom )) return imagelist def optikern( image, width, zoom ): # optical kerning algorithm left = [] right = [] resolution = 8 for i in range( 0, 18 * resolution ): y = 1.0/resolution * (i + 0.5) * zoom xmin = None xmax = None for cmd,params in image: segmentBox = None if cmd == 'M': # A move cannot contribute to the bounding box last = params[:] lastctrl = params[:] elif cmd == 'L': if (y >= last[1] and y <= params[1]) or (y >= params[1] and y <= last[1]): if params[0] == last[0]: x = params[0] else: a = (params[1] - last[1]) / (params[0] - last[0]) b = last[1] - a * last[0] if a != 0: x = (y - b) / a else: x = None if x: if xmin == None or x < xmin: xmin = x if xmax == None or x > xmax: xmax = x last = params[:] lastctrl = params[:] elif cmd == 'C': if last: bx0, by0 = last[:] bx1, by1, bx2, by2, bx3, by3 = params[:] d = by0 - y c = -3*by0 + 3*by1 b = 3*by0 - 6*by1 + 3*by2 a = -by0 + 3*by1 - 3*by2 + by3 ts = findRealRoots(a, b, c, d) for t in ts: if t >= 0 and t <= 1: x = (-bx0 + 3*bx1 - 3*bx2 + bx3)*(t**3) + \ (3*bx0 - 6*bx1 + 3*bx2)*(t**2) + \ (-3*bx0 + 3*bx1)*t + \ bx0 if xmin == None or x < xmin: xmin = x if xmax == None or x > xmax: xmax = x last = params[-2:] lastctrl = params[2:4] elif cmd == 'Q': # Quadratic beziers are ignored last = params[-2:] lastctrl = params[2:4] elif cmd == 'A': # Arcs are ignored last = params[-2:] lastctrl = params[2:4] if xmin != None and xmax != None: left.append( xmin ) # distance from left edge of region to left edge of bbox right.append( width - xmax ) # distance from right edge of region to right edge of bbox else: left.append( width ) right.append( width ) return (left, right) def layoutstring( imagelist, zoom ): # layout string of letter-images using optical kerning kernlist = [] length = zoom for entry in imagelist: if (entry == " "): # leaving room for " " space characters length = length + (zoom * render_alphabetsoup_config.space) else: image, width, height = entry length = length + width + zoom # add letter length to overall length kernlist.append( optikern(image, width, zoom) ) # append kerning data for this image workspace = None position = zoom for i in range(0, len(kernlist)): while(imagelist[i] == " "): position = position + (zoom * render_alphabetsoup_config.space ) imagelist.pop(i) image, width, height = imagelist[i] # set the kerning if i == 0: kern = 0 # for first image, kerning is zero else: kerncompare = [] # kerning comparison array for j in range( 0, len(kernlist[i][0])): kerncompare.append( kernlist[i][0][j]+kernlist[i-1][1][j] ) kern = min( kerncompare ) position = position - kern # move position back by kern amount thisimage = copy.deepcopy(image) simplepath.translatePath(thisimage, position, 0) workspace = combinePaths(workspace, thisimage) position = position + width + zoom # advance position by letter width return workspace def tokenize(text): """Tokenize the string, looking for LaTeX style, multi-character tokens in the string, like \\yogh.""" tokens = [] i = 0 while i < len(text): c = text[i] i += 1 if c == '\\': # found the beginning of an escape t = '' while i < len(text): # gobble up content of the escape c = text[i] if c == '\\': # found another escape, stop this one break i += 1 if c == ' ': # a space terminates this escape break t += c # stick this character onto the token if t: tokens.append(t) else: tokens.append(c) return tokens class AlphabetSoup(inkex.Effect): def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("-t", "--text", action="store", type="string", dest="text", default="Inkscape", help="The text for alphabet soup") self.OptionParser.add_option("-z", "--zoom", action="store", type="float", dest="zoom", default="8.0", help="The zoom on the output graphics") self.OptionParser.add_option("-r", "--randomize", action="store", type="inkbool", dest="randomize", default=False, help="Generate random (unreadable) text") def effect(self): zoom = self.unittouu( str(self.options.zoom) + 'px') if self.options.randomize: imagelist = generate_random_string(self.options.text, zoom) else: tokens = tokenize(self.options.text) imagelist = randomize_input_string(tokens, zoom) image = layoutstring( imagelist, zoom ) if image: s = { 'stroke': 'none', 'fill': '#000000' } new = inkex.etree.Element(inkex.addNS('path','svg')) new.set('style', simplestyle.formatStyle(s)) new.set('d', simplepath.formatPath(image)) self.current_layer.append(new) if __name__ == '__main__': e = AlphabetSoup() e.affect()