From b0b3026b6dd688e0525a65eba6a013cc1587c115 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Wed, 16 Jun 2021 16:14:04 +0200 Subject: [PATCH] Added Incadiff extension --- .../fablabchemnitz/incadiff/incadiff.inx | 17 + .../fablabchemnitz/incadiff/incadiff.py | 359 ++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 extensions/fablabchemnitz/incadiff/incadiff.inx create mode 100644 extensions/fablabchemnitz/incadiff/incadiff.py diff --git a/extensions/fablabchemnitz/incadiff/incadiff.inx b/extensions/fablabchemnitz/incadiff/incadiff.inx new file mode 100644 index 00000000..adefde53 --- /dev/null +++ b/extensions/fablabchemnitz/incadiff/incadiff.inx @@ -0,0 +1,17 @@ + + + Incadiff + fablabchemnitz.de.incadiff + + + Apply successive difference operations on superimposed paths. Useful for plotter addicts. + all + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/incadiff/incadiff.py b/extensions/fablabchemnitz/incadiff/incadiff.py new file mode 100644 index 00000000..f01f3405 --- /dev/null +++ b/extensions/fablabchemnitz/incadiff/incadiff.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# coding=utf-8 + +""" +Copyright (C) 2021 Thomas Maziere + +Largely and mostly inspired by inx-pathops (https://gitlab.com/moini_ink/inx-pathops/) +Copyright (C) 2014 Ryan Lerch (multiple difference) + 2016 Maren Hachmann + (refactoring, extend to multibool) + 2017 su_v + Rewrite to support large selections (process in chunks), to + improve performance (support groups, z-sort ids with python + instead of external query), and to extend GUI options. + + +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. +""" + +""" +incadiff +Apply successive difference operations on superimposed paths. Useful for plotter addicts. +""" + +import os +from shutil import copy2 +import time +from lxml import etree + + +import inkex +import inkex.command + +__version__ = '0.2' + +# Global "constants" +SVG_SHAPES = ('rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon') + + +# ----- general helper functions + +def timed(f): + """Minimalistic timer for functions.""" + # pylint: disable=invalid-name + start = time.time() + ret = f() + elapsed = time.time() - start + return ret, elapsed + + +def get_inkscape_version(): + ink = inkex.command.INKSCAPE_EXECUTABLE_NAME + try: # needed prior to 1.1 + ink_version = inkex.command.call(ink, '--version').decode("utf-8") + except AttributeError: # needed starting from 1.1 + ink_version = inkex.command.call(ink, '--version') + + pos = ink_version.find("Inkscape ") + if pos != -1: + pos += 9 + else: + return None + v_num = ink_version[pos:pos+3] + return(v_num) + +# ----- SVG element helper functions + + +def get_defs(node): + """Find in children of *node*, return first one found.""" + path = '/svg:svg//svg:defs' + try: + return node.xpath(path, namespaces=inkex.NSS)[0] + except IndexError: + return etree.SubElement(node, inkex.addNS('defs', 'svg')) + + +def is_group(node): + """Check node for group tag.""" + return node.tag == inkex.addNS('g', 'svg') + + +def is_path(node): + """Check node for path tag.""" + return node.tag == inkex.addNS('path', 'svg') + + +def is_basic_shape(node): + """Check node for SVG basic shape tag.""" + return node.tag in (inkex.addNS(tag, 'svg') for tag in SVG_SHAPES) + + +def is_custom_shape(node): + """Check node for Inkscape custom shape type.""" + return inkex.addNS('type', 'sodipodi') in node.attrib + + +def is_shape(node): + """Check node for SVG basic shape tag or Inkscape custom shape type.""" + return is_basic_shape(node) or is_custom_shape(node) + + +def has_path_effect(node): + """Check node for Inkscape path-effect attribute.""" + return inkex.addNS('path-effect', 'inkscape') in node.attrib + + +def is_modifiable_path(node): + """Check node for editable path data.""" + return is_path(node) and not (has_path_effect(node) or + is_custom_shape(node)) + + +def is_image(node): + """Check node for image tag.""" + return node.tag == inkex.addNS('image', 'svg') + + +def is_text(node): + """Check node for text tag.""" + return node.tag == inkex.addNS('text', 'svg') + + +def does_pathops(node): + """Check whether node is supported by Inkscape path operations.""" + return (is_path(node) or + is_shape(node) or + is_text(node)) + + +# ----- list processing helper functions + +def recurse_selection(node, id_list, level=0, current=0): + """Recursively process selection, add checked elements to id list.""" + current += 1 + if not level or current <= level: + if is_group(node): + for child in node: + id_list = recurse_selection(child, id_list, level, current) + if does_pathops(node): + id_list.append(node.get('id')) + return id_list + + +def z_sort(node, alist): + """Return new list sorted in document order (depth-first traversal).""" + ordered = [] + id_list = list(alist) + count = len(id_list) + for element in node.iter(): + element_id = element.get('id') + if element_id is not None and element_id in id_list: + id_list.remove(element_id) + ordered.append(element_id) + count -= 1 + if not count: + break + return ordered + + +def z_iter(node, alist): + """Return iterator over ids in document order (depth-first traversal).""" + id_list = list(alist) + for element in node.iter(): + element_id = element.get('id') + if element_id is not None and element_id in id_list: + id_list.remove(element_id) + yield element_id + + +def chunks(alist, max_len): + """Chunk a list into sublists of max_len length.""" + for i in range(0, len(alist), max_len): + yield alist[i:i+max_len] + + +class IncadiffExtension(inkex.Effect): + + def __init__(self): + inkex.Effect.__init__(self) + self.actions_list = [] + + def add_arguments(self, pars): + pars.add_argument("--my_option", type=inkex.Boolean, + help="An example option, put your options here") + + def get_selected_ids(self): + """Return a list of valid ids for inkscape path operations.""" + id_list = [] + if len(self.svg.selected) == 0: + pass + else: + level = 0 + for node in self.svg.selected.values(): + recurse_selection(node, id_list, level) + if len(id_list) < 2: + inkex.errormsg("This extension requires at least 2 elements " + + "of type path, shape or text. " + + "The elements can be part of selected groups, " + + "or directly selected.") + return None + if len(id_list) > 64: + inkex.errormsg("You should not select more than 64 shapes/paths, " + + "and ideally you should apply this extension to small groups of objects.") + return None + + else: + return id_list + + def get_sorted_ids(self): + """Return a list with z-sorted ids.""" + sorted_ids = None + id_list = self.get_selected_ids() + if id_list is not None: + sorted_ids = list(z_iter(self.document.getroot(), id_list)) + return (sorted_ids) + else: + return + + + def run_cmd(self, tempfile): + #ink_version = get_inkscape_version() + self.actions_list.append("FileSave") + extra_param = "--batch-process" + # if ink_version == "1.0": + # self.actions_list.append("FileQuit") + # extra_param = "--with-gui" + actions = ";".join(self.actions_list) + inkex.command.inkscape(tempfile, extra_param, actions=actions) + + def duplicate_and_diff(self, id_list, tempfile): + # for each selected path, duplicate and diff for each path below + nb_shapes = len(id_list) + for i in range(0, nb_shapes): + top_path = id_list[i] + j = i + while j > 0: + j -= 1 + self.actions_list.append("select-by-id:"+top_path) + self.actions_list.append("EditDuplicate") + self.actions_list.append("select-by-id:" + id_list[j]) + self.actions_list.append("SelectionDiff") + self.actions_list.append("EditDeselect") + if nb_shapes > 20: + self.run_cmd(tempfile) + self.actions_list = [] + if len(self.actions_list) > 0: + self.run_cmd(tempfile) + self.actions_list = [] + + def loop_diff(self): + """Loop through selected items and run external command(s).""" + + tempfile = self.options.input_file + "-incadiff.svg" + # prepare + # we need to do this because command line Inkscape with gui + # gives lots of info dialogs when the file extension isn't 'svg' + # so the inkscape() call cannot open the file without user + # interaction, and fails in the end when trying to save + copy2(self.options.input_file, tempfile) + + # loop through selected paths + id_list = self.get_sorted_ids() + if id_list is not None: + self.duplicate_and_diff(id_list, tempfile) + else: + return + + + # replace current document with content of temp copy file + self.document = inkex.load_svg(tempfile) + # update self.svg + self.svg = self.document.getroot() + + # purge missing tagrefs (see below) + self.update_tagrefs() + # clean up + self.cleanup(tempfile) + + def cleanup(self, tempfile): + """Clean up tempfile.""" + try: + os.remove(tempfile) + except Exception: # pylint: disable=broad-except + pass + + def effect(self): + if self.has_tagrefs(): + # unsafe to use with extensions ... + inkex.utils.errormsg("This document uses Inkscape selection sets. " + + "Modifying the content with this extension " + + "may cause Inkscape to crash on reload or close. " + + "Please delete the selection sets, " + + "save the document under a new name and " + + "try again in a new Inkscape session.") + else: + self.loop_diff() + + def has_tagrefs(self): + """Check whether document has selection sets with tagrefs.""" + defs = get_defs(self.document.getroot()) + inkscape_tagrefs = defs.findall( + "inkscape:tag/inkscape:tagref", namespaces=inkex.NSS) + return len(inkscape_tagrefs) > 0 + + def update_tagrefs(self, mode='purge'): + """Check tagrefs for deleted objects.""" + defs = get_defs(self.document.getroot()) + inkscape_tagrefs = defs.findall( + "inkscape:tag/inkscape:tagref", namespaces=inkex.NSS) + if len(inkscape_tagrefs) > 0: + for tagref in inkscape_tagrefs: + href = tagref.get(inkex.addNS('href', 'xlink'))[1:] + if self.svg.getElementById(href) is None: + if mode == 'purge': + tagref.getparent().remove(tagref) + elif mode == 'placeholder': + temp = etree.Element(inkex.addNS('path', 'svg')) + temp.set('id', href) + temp.set('d', 'M 0,0 Z') + self.document.getroot().append(temp) + + # ----- workaround to fix Effect() performance with large selections + + def collect_ids(self, doc=None): + """Iterate all elements, build id dicts (doc_ids, selected).""" + doc = self.document if doc is None else doc + id_list = list(self.options.ids) + for node in doc.getroot().iter(tag=etree.Element): + if 'id' in node.attrib: + node_id = node.get('id') + self.doc_ids[node_id] = 1 + if node_id in id_list: + self.svg.selected[node_id] = node + id_list.remove(node_id) + + def getselected(self): + """Overload Effect() method.""" + self.collect_ids() + + def getdocids(self): + """Overload Effect() method.""" + pass + + +if __name__ == '__main__': + IncadiffExtension().run()