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()