#!/usr/bin/env python # coding=utf-8 # # 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # import cmath import copy import math import os import random import re import sys import inkex from inkex import Vector2d, load_svg import render_alphabetsoup_config syntax = render_alphabetsoup_config.syntax alphabet = render_alphabetsoup_config.alphabet units = render_alphabetsoup_config.units font = render_alphabetsoup_config.font def load_path(filename): """Loads a super-path from a given SVG file""" base = 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. fullpath = os.path.join(base, filename) tree = load_svg(fullpath) root = tree.getroot() elem = root.findone("svg:path") if elem is None: return None, 0, 0 width = float(root.get("width")) height = float(root.get("height")) return ( elem.path.to_arrays(), width, height, ) # Currently we only support a single path def combinePaths(pathA, pathB): if pathA is None and pathB is None: return None elif pathA is None: return pathB elif pathB is 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 _lr_cb(p, width): p.scale(-1, 1) p.translate(width, 0) def _tp_cb(p, height): p.scale(1, -1) p.translate(0, height) def flip(sp, cb, param): # print('flip before +' + str(sp)) p = inkex.Path(sp) cb(p, param) del sp[:] prev = Vector2d() prev_prev = Vector2d() first = Vector2d() for i, seg in enumerate(p): if i == 0: first = seg.end_point(first, prev) cps = [] for cp in seg.control_points(first, prev, prev_prev): prev_prev = prev prev = cp cps.extend(cp) sp.append([seg.letter, cps]) # print('flip after +' + str(sp)) def flipLeftRight(sp, width): return flip(sp, _lr_cb, width) def flipTopBottom(sp, height): return flip(sp, _tp_cb, height) 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 isinstance(u, complex): # 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 isinstance(y2, complex): # 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 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 = load_path(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 = inkex.Path(currimg).bounding_box() dx = rule[i][1] * units dy = rule[i][2] * units # newbox = ((box[0]+dx),(box[1]+dy),(box[2]+dx),(box[3]+dy)) currimg = (inkex.Path(currimg).translate(dx, dy)).to_arrays() 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 = inkex.Path(image).bounding_box() image = (inkex.Path(image).translate(-bbox.x.minimum, 0)).to_arrays() image = (inkex.Path(image).scale(zoom / units, zoom / units)).to_arrays() return image, bbox.width, bbox.height def randomize_input_string( tokens, zoom ): # generate a glyph starting from each token in the input string imagelist = [] stack = None for i in range(0, len(tokens)): char = tokens[i] # if ( re.match("[a-zA-Z0-9?]", char)): if char in alphabet: 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 = alphabet[char][ random.randint(0, (len(alphabet[char]) - 1)) ].split(".") # 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 "{}"\n'.format(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"{}"\n'.format(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: if cmd == "M": # A move cannot contribute to the bounding box last = params[:] lastctrl = params[:] elif cmd == "L": if (last[1] <= y <= params[1]) or (params[1] <= 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 is None or x < xmin: xmin = x if xmax is 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 0 <= 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 is None or x < xmin: xmin = x if xmax is 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 is not None and xmax is not 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) thisimage = (inkex.Path(thisimage).translate(position, 0)).to_arrays() 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.EffectExtension): def add_arguments(self, pars): pars.add_argument( "-t", "--text", default="Inkscape", help="The text for alphabet soup" ) pars.add_argument( "-z", "--zoom", type=float, default=8.0, help="The zoom on the output" ) pars.add_argument( "-r", "--randomize", type=inkex.Boolean, default=False, help="Generate random (unreadable) text", ) def effect(self): zoom = self.svg.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.PathElement(style=str(inkex.Style(s)), d=str(inkex.Path(image))) layer = self.svg.get_current_layer() layer.append(new) # compensate preserved transforms of parent layer if layer.getparent() is not None: mat = ( self.svg.get_current_layer().transform @ inkex.Transform([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) ).matrix new.transform @= -inkex.Transform(mat) if __name__ == "__main__": AlphabetSoup().run()