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/text_split.py | |
parent | Initial commit. (diff) | |
download | inkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.tar.xz inkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.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/text_split.py')
-rwxr-xr-x | share/extensions/text_split.py | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/share/extensions/text_split.py b/share/extensions/text_split.py new file mode 100755 index 0000000..dee9c88 --- /dev/null +++ b/share/extensions/text_split.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python +# coding=utf-8 +# +# Copyright (C) 2009 Karlisson Bezerra, contato@nerdson.com +# 2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com +# +# 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. +# +"""Splits a text element into lines, words, chars. +Supports all text elements that Inkscape can create, such as normal text, shape-inside (SVG2), +flowroot (SVG1.2), inline-size, manual kerns, and nested tspans (with possibly different kerns) + +The code is structured as followed. For each selected text element: + - preprocess_text_element duplicates the element, converts flowroots to plain text elements, + and simplifies manual kerning if requested (only for split_words and split_chars, for all, + possibly nested, children) using simplify_nested_tspans. + - if split lines: split_lines copies all top-level tspans from the previous step into their own + text element, which is otherwise a duplicate of the original text element (thus preserving + style and transforms), see append_splitted_element + - if split words or chars: split_words_or_chars: the text is recursively processed. For each tspan, + the content and tail is split (words: at spaces, chars: after each character) into their own + tspan, again using append_splitted_element. The method keeps track of the horizontal and vertical + coordinate, incrementing it with the number of characters and a multiple of font size. +""" + +import re as regex +from typing import Union, Callable + +import inkex +from inkex import TextElement, FlowRoot, FlowPara, Tspan, Rectangle, ShapeElement +from inkex.units import parse_unit +from inkex.localization import inkex_gettext as _ + +TextLike = Union[FlowRoot, TextElement] + + +class TextSplit(inkex.EffectExtension): + """Split text up.""" + + def __init__(self): + """Initialize State machine""" + super().__init__() + self.mode: Callable + self.separation: float = 1 + self.fs_multiplier: float = 0.25 + self.current_x: float = 0 + self.current_y: float = 0 + self.process_kerns: bool = True + self.current_root: TextLike + self.current_fontsize: float = 0 + + def add_arguments(self, pars): + pars.add_argument("--tab", help="The selected UI tab when OK was pressed") + pars.add_argument( + "-t", + "--splittype", + default="line", + choices=["letter", "word", "line"], + help="type of split", + ) + pars.add_argument( + "-p", + "--preserve", + type=inkex.Boolean, + default=True, + help="Preserve original", + ) + pars.add_argument( + "-s", + "--separation", + type=float, + default=1, + help="Threshold for separating text with manual kerns in multiples of" + "font-size", + ) + + def effect(self): + """Applies the effect""" + + split_type = self.options.splittype + preserve = self.options.preserve + + # checks if the selected elements are text nodes + for elem in self.svg.selection.filter_nonzero(TextElement, FlowRoot): + try: + self.separation = self.options.separation + if split_type == "line": + node = self.split_lines(elem) + elif split_type == "word": + self.mode = self.process_plain_words + node = self.split_words_or_chars(elem) + else: + self.separation = 0 + self.mode = self.process_plain_chars + node = self.split_words_or_chars(elem) + + node.getparent().remove(node) + + if not preserve and node is not None: + elem.getparent().remove(elem) + except TypeError as err: + inkex.errormsg(err) # if an element can not be processed + + @staticmethod + def get_font_size(element): + """get the font size of an element""" + return element.specified_style()("font-size") + + @staticmethod + def get_line_height(element: ShapeElement): + """get the line height of an element""" + return element.get_line_height_uu() + + def simplify_child_tspans(self, element: TextElement): + """Checks all child tspans if they have manual kerns. + If it does, try to find words (characters with a distance > separation * font-size). + Then concatenate the words with spaces, set this string as a new text and""" + for child in list(element): + # process manual kerns + if not isinstance(child, Tspan): + continue + xvals = list( + map(float, filter(len, regex.split(r"[,\s]", child.get("x") or ""))) + ) + content = child.text + if content not in [None, ""] and len(xvals) >= 2: + fsize = self.get_font_size(child) + separation = self.separation * fsize + current_word_start = 0 + for i in range(1, max(len(content), len(xvals))): + if i >= len(content) - 1 or i >= len(xvals) - 1: + # consume the entire remaining string + i = len(content) + if i == len(content) or abs(xvals[i] - xvals[i - 1]) > separation: + wordspan = Tspan(x=str(xvals[current_word_start])) + wordspan.text = content[current_word_start:i] + child.add(wordspan) + current_word_start = i + child.pop("x") + child.text = None + # process child elements + self.simplify_child_tspans(child) + + def preprocess_text_element(self, element: TextElement): + """Processes a text element and returns an element containing tspans with x and y coordinate, + possibly nested (for Inkscape-type kerning), so that the actual splitting can work as if the + text was a simple text. Manual kerns (one x value per letter) are converted to spaces + if requested (not necessary for "split characters")""" + + oldelement = element + if isinstance(element, FlowRoot): + element = TextElement() + oldelement.addnext(element) + element.style = oldelement.style + element.transform = oldelement.transform + flowref = oldelement.findone("svg:flowRegion")[0] + if isinstance(flowref, Rectangle): + flowx = element.unittouu(flowref.get("x")) + flowy = element.unittouu(float(flowref.get("y"))) + first = True + else: + raise TypeError( + _( + "Element {} uses a flow region that is not a rectangle. " + "First unflow text." + ).format(element.get_id()) + ) + for child in oldelement: + if isinstance(child, FlowPara): + # convert the flowpara "line" (note: no automatic wrapping) + # to a tspan and set the y coordinate. + # future FlowRoot improvements could add a better conversion. + newchild = Tspan() + element.append(newchild) + newchild.text = child.text + newchild.style = child.style + newchild.transform = child.transform + newchild.set("x", flowx) + if first: + flowy += self.get_font_size(child) * 1.25 + first = False + else: + flowy += self.get_line_height(child) + newchild.set("y", str(flowy)) + + else: + element = oldelement.duplicate() + oldelement.getparent().append(element) + + element.style.pop("shape-inside", None) + + # Real support for RTL text is missing, but we can emulate it by just removing the + # attribute. However, line breaks will be misaligned. + element.style.pop("direction", None) + for child in element: + child.style.pop("direction", None) + + if self.process_kerns: + self.simplify_child_tspans(element) + return element + + def append_splitted_element(self, text, prototype=None): + """Creates a new text element, sibling to self.current_root, at (self.current_x, + self.current_y) with content text. + + text: either a Tspan that should be moved to a new text element - in this case, text is + a direct child of element; or a string + prototype: if text is a string, style and transform will be taken from prototype""" + + if isinstance(text, Tspan) and text.getparent() == self.current_root: + # we just move the tspan to a new text element. + elem = self.current_root.duplicate() + elem.remove_all(Tspan) + elem.append(text) + elem.set("x", text.get("x")) + elem.set("y", text.get("y")) + else: + elem = TextElement(x=str(self.current_x), y=str(self.current_y)) + # transfer the style from all parents, including the text element (if there's a style to + # the text element's parent applied, it will be duplicated, but that doesn't really + # matter) + elem.style = prototype.specified_style() + # the element will be appended to the parent of element, but there might be nested + # tspans between the prototype and the element. The next line says + # "compose transforms until you reach the parent of element" + elem.transform = ( + -self.current_root.getparent().transform + ) @ prototype.composed_transform() + tsp = Tspan(x=str(self.current_x), y=str(self.current_y)) + tsp.text = text + elem.add(tsp) + self.current_root.addnext(elem) + + def split_lines(self, element: TextLike) -> TextElement: + """Splits a text into its lines""" + self.process_kerns = False + preprocessed = self.preprocess_text_element(element) + self.current_root = preprocessed + # Now we only have to copy each tspan into its own text element. + for child in list(preprocessed): + self.append_splitted_element(child) + + return preprocessed + + def process_plain_text(self, element, splitted): + """Appends new text elements to as sibling root for each element of splitted, starting at + self.current_x, self.current_y, incrementing those, with prototype element (that + styles and transforms will be taken from)""" + if splitted is None: + return + for word in splitted: + if word != "": + self.append_splitted_element(word, element) + # +1 since for words, we lost a space + self.current_x += ( + self.current_fontsize * (len(word) + 1) * self.fs_multiplier + ) + + def process_plain_words(self, element, text): + """Calls process_plain_text for splitting words""" + self.fs_multiplier = 0.4 + if text is not None: + self.process_plain_text(element, text.split(" ")) + + def process_plain_chars(self, element, text): + """Calls process_plain_text for splitting characters""" + self.fs_multiplier = 0.25 + self.process_plain_text(element, text) + + def split_words_or_chars(self, element: TextLike) -> TextElement: + """Splits a text into its lines""" + self.process_kerns = True + preprocessed = self.preprocess_text_element(element) + + def process_element(element) -> float: + elem_coords = { + i: element.root.unittouu(element.get(i)) + if element.get(i) is not None + else None + for i in "xy" + } + if elem_coords["x"] is not None: + self.current_x = elem_coords["x"] + if elem_coords["y"] is not None: + self.current_y = elem_coords["y"] + self.current_fontsize = self.get_font_size(element) + current_x = self.mode(element, element.text) + + for elem in element: + if isinstance(elem, Tspan): + current_x = process_element(elem) + current_x = self.mode(element, elem.tail) + return current_x + + self.current_root = preprocessed + process_element(preprocessed) + return preprocessed + + +if __name__ == "__main__": + TextSplit().run() |