diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/render_alphabetsoup.py | |
parent | Initial commit. (diff) | |
download | inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'share/extensions/render_alphabetsoup.py')
-rwxr-xr-x | share/extensions/render_alphabetsoup.py | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/share/extensions/render_alphabetsoup.py b/share/extensions/render_alphabetsoup.py new file mode 100755 index 0000000..825c285 --- /dev/null +++ b/share/extensions/render_alphabetsoup.py @@ -0,0 +1,542 @@ +#!/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() |