diff --git a/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.inx b/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.inx
new file mode 100644
index 0000000..2ce0bac
--- /dev/null
+++ b/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.inx
@@ -0,0 +1,72 @@
+
+
+ Cleanup Styles
+ fablabchemnitz.de.cleanup_styles
+
+
+
+
+
+
+
+ false
+ 0.100
+
+
+
+
+
+
+
+ false
+ 100.0
+ true
+ true
+ true
+ true
+ true
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../000_about_fablabchemnitz.svg
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.py b/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.py
new file mode 100644
index 0000000..bc68179
--- /dev/null
+++ b/extensions/fablabchemnitz/cleanup_styles/cleanup_styles.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+'''
+Copyright (C) 2013 Matthew Dockrey (gfish @ cyphertext.net)
+
+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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Based on
+- coloreffect.py by Jos Hirth and Aaron C. Spike
+- cleanup.py (https://github.com/attoparsec/inkscape-extensions) by attoparsec
+
+Author: Mario Voigt / FabLab Chemnitz
+Mail: mario.voigt@stadtfabrikanten.org
+Last Patch: 12.04.2021
+License: GNU GPL v3
+
+Notes:
+ - This extension does not check if attributes contain duplicates properties like "opacity:1;fill:#393834;fill-opacity:1;opacity:1;fill:#393834;fill-opacity:1". We assume the SVG syntax is correct
+'''
+
+import inkex
+import re
+import numpy as np
+
+class CleanupStyles(inkex.EffectExtension):
+
+ groups = []
+ roundUpColors = []
+ roundUpColors = [
+ [ 0, 0, 0], #black | eri1
+ [ 0, 0, 255], #blue | eri2
+ [ 0, 255, 0], #green | eri3
+ [255, 0, 0], #red | eri4
+ [255, 0, 255], #magenta | eri5
+ [ 0, 255, 255], #cyan | eri6
+ [255, 255, 0], #yellow | eri7
+ #half tones
+ [128, 0, 255], #violet | eri8
+ [0 , 128, 255], #light blue | eri9
+ [255, 128, 0], #orange | eri10
+ [255, 0, 128], #pink | eri11
+ [128, 255, 0], #light green | eri12
+ [0 , 255, 128], #mint | eri13
+ ]
+
+ def add_arguments(self, pars):
+ pars.add_argument("--tab")
+ pars.add_argument("--mode", default="Lines", help="Join paths with lines or polygons")
+ pars.add_argument("--dedicated_style_attributes", default="ignore", help="Handling of dedicated style attributes")
+ pars.add_argument("--stroke_width_override", type=inkex.Boolean, default=False, help="Override stroke width")
+ pars.add_argument("--stroke_width", type=float, default=0.100, help="Stroke width")
+ pars.add_argument("--stroke_width_units", default="mm", help="Stroke width unit")
+ pars.add_argument("--stroke_opacity_override", type=inkex.Boolean, default=False, help="Override stroke opacity")
+ pars.add_argument("--stroke_opacity", type=float, default="100.0", help="Stroke opacity (%)")
+ pars.add_argument("--reset_opacity", type=inkex.Boolean, default=True, help="Reset stroke style attribute 'opacity'. Do not mix up with 'fill-opacity' and 'stroke-opacity'")
+ pars.add_argument("--reset_stroke_attributes", type=inkex.Boolean, default=True, help="Remove 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linejoin', 'stroke-linecap', 'stroke-miterlimit' from style attribute")
+ pars.add_argument("--reset_fill_attributes", type=inkex.Boolean, default=True, help="Sets 'fill:none;fill-opacity:1;' to style attribute")
+ pars.add_argument("--apply_hairlines", type=inkex.Boolean, default=True, help="Adds 'vector-effect:non-scaling-stroke;' and '-inkscape-stroke:hairline;' Hint: stroke-width is kept in background. All hairlines still have a valued width.")
+ pars.add_argument("--apply_black_strokes", type=inkex.Boolean, default=True, help="Adds 'stroke:#000000;' to style attribute")
+ pars.add_argument("--remove_group_styles", type=inkex.Boolean, default=False, help="Remove styles from groups")
+ pars.add_argument("--harmonize_colors", type=inkex.Boolean, default=False, help="Round up colors to the next 'full color'. Example: make rgb(253,0,0) to rgb(255,0,0) to receive clear red color.")
+ pars.add_argument("--allow_half_tones", type=inkex.Boolean, default=False, help="Allow rounding up to half-tone colors")
+
+
+ def closestColor(self, colors, color):
+ colors = np.array(colors)
+ color = np.array(color)
+ distances = np.sqrt(np.sum((colors-color)**2, axis=1))
+ index_of_smallest = np.where(distances==np.amin(distances))
+ smallest_distance = colors[index_of_smallest]
+ return smallest_distance
+
+ def effect(self):
+ self.roundUpColors = [
+ [ 0, 0, 0], #black | eri1
+ [ 0, 0, 255], #blue | eri2
+ [ 0, 255, 0], #green | eri3
+ [255, 0, 0], #red | eri4
+ [255, 0, 255], #magenta | eri5
+ [ 0, 255, 255], #cyan | eri6
+ [255, 255, 0], #yellow | eri7
+ [255, 255, 255], #white | eri8 - useful for engravings, not for line cuttings
+ ]
+ if self.options.allow_half_tones is True:
+ self.roundUpColors.extend([
+ [128, 0, 255], #violet | eri9
+ [0 , 128, 255], #light blue | eri10
+ [255, 128, 0], #orange | eri11
+ [255, 0, 128], #pink | eri12
+ [128, 255, 0], #light green | eri13
+ [128, 255, 255], #lighter blue| eri14
+ [0 , 255, 128], #mint | eri15
+ [128, 128, 128], #grey | eri16
+ ])
+
+ if len(self.svg.selected) == 0:
+ self.getAttribs(self.document.getroot())
+ else:
+ for element in self.svg.selected.values():
+ self.getAttribs(element)
+ #finally remove the styles from collected groups (if enabled)
+ if self.options.remove_group_styles is True:
+ for group in self.groups:
+ if group.attrib.has_key('style') is True:
+ group.attrib.pop('style')
+
+ def getAttribs(self, node):
+ self.changeStyle(node)
+ for child in node:
+ self.getAttribs(child)
+
+ #stroke and fill styles can be included in style attribute or they can exist separately (can occure in older SVG files). We do not parse other attributes than style
+ def changeStyle(self, node):
+ #we check/modify the style of all shapes (not groups)
+ if isinstance(node, inkex.ShapeElement) and not isinstance(node, inkex.Group):
+ # the final styles applied to this element (with influence from top level elements like groups)
+ specified_style = node.specified_style()
+ specifiedStyleAttributes = str(specified_style).split(';') #array
+ specifiedStyleAttributesDict = {}
+ if len(specified_style) > 0: #Style "specified_style" might contain just empty '' string which will lead to failing dict update
+ for specifiedStyleAttribute in specifiedStyleAttributes:
+ specifiedStyleAttributesDict.update({'{}'.format(specifiedStyleAttribute.split(':')[0]): specifiedStyleAttribute.split(':')[1]})
+
+ #three options to handle dedicated attributes (attributes not in the "style" attribute, but separate):
+ # - just delete all dedicated properties
+ # - merge dedicated properties, and prefer them over those from specified style
+ # - merge dedicated properties, but prefer properties from specified style
+ dedicatedStyleAttributesDict = {}
+ popDict = []
+ # there are opacity (of group/parent), fill-opacity and stroke-opacity
+ popDict.extend(['opacity', 'stroke', 'stroke-opacity', 'stroke-width', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'fill', 'fill-opacity'])
+ for popItem in popDict:
+ if node.attrib.has_key(str(popItem)):
+ dedicatedStyleAttributesDict.update({'{}'.format(popItem): node.get(popItem)})
+ node.attrib.pop(popItem)
+
+ #inkex.utils.debug("specifiedStyleAttributesDict = " + str(specifiedStyleAttributesDict))
+ #inkex.utils.debug("dedicatedStyleAttributesDict = " + str(dedicatedStyleAttributesDict))
+
+ if self.options.dedicated_style_attributes == 'prefer_dedicated':
+ specifiedStyleAttributesDict.update(dedicatedStyleAttributesDict)
+ node.set('style', specifiedStyleAttributesDict)
+ elif self.options.dedicated_style_attributes == 'prefer_specified':
+ dedicatedStyleAttributesDict.update(specifiedStyleAttributesDict)
+ node.set('style', dedicatedStyleAttributesDict)
+ elif self.options.dedicated_style_attributes == 'ignore':
+ pass
+
+ # now parse the style with possibly merged dedicated attributes modded style attribute (dedicatedStyleAttributes)
+ if node.attrib.has_key('style') is False:
+ node.set('style', 'stroke:#000000;') #we add basic stroke color black. We cannot set to empty value or just ";" because it will not update properly
+ style = node.get('style')
+
+ #add missing style attributes if required
+ if style.endswith(';') is False:
+ style += ';'
+ if re.search('(;|^)stroke:(.*?)(;|$)', style) is None: #if "stroke" is None, add one. We need to distinguish because there's also attribute "-inkscape-stroke" that's why we check starting with ^ or ;
+ style += 'stroke:none;'
+ if self.options.stroke_width_override is True and "stroke-width:" not in style:
+ style += 'stroke-width:{:1.4f};'.format(self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_width_units))
+ if self.options.stroke_opacity_override is True and "stroke-opacity:" not in style:
+ style += 'stroke-opacity:{:1.1f};'.format(self.options.stroke_opacity / 100)
+
+ if self.options.apply_hairlines is True:
+ if "vector-effect:non-scaling-stroke" not in style:
+ style += 'vector-effect:non-scaling-stroke;'
+ if "-inkscape-stroke:hairline" not in style:
+ style += '-inkscape-stroke:hairline;'
+
+ if re.search('fill:(.*?)(;|$)', style) is None: #if "fill" is None, add one.
+ style += 'fill:none;'
+
+ #then parse the content and check what we need to adjust
+ declarations = style.split(';')
+ for i, decl in enumerate(declarations):
+ parts = decl.split(':', 2)
+ if len(parts) == 2:
+ (prop, val) = parts
+ prop = prop.strip().lower()
+ if prop == 'stroke-width' and self.options.stroke_width_override is True:
+ new_val = self.svg.unittouu(str(self.options.stroke_width) + self.options.stroke_width_units)
+ declarations[i] = prop + ':{:1.4f}'.format(new_val)
+ if prop == 'stroke-opacity' and self.options.stroke_opacity_override is True:
+ new_val = self.options.stroke_opacity / 100
+ declarations[i] = prop + ':{:1.1f}'.format(new_val)
+ if self.options.reset_opacity is True:
+ if prop == 'opacity':
+ declarations[i] = ''
+ if self.options.reset_stroke_attributes is True:
+ if prop == 'stroke-dasharray':
+ declarations[i] = ''
+ if prop == 'stroke-dashoffset':
+ declarations[i] = ''
+ if prop == 'stroke-linejoin':
+ declarations[i] = ''
+ if prop == 'stroke-linecap':
+ declarations[i] = ''
+ if prop == 'stroke-miterlimit':
+ declarations[i] = ''
+ if self.options.apply_black_strokes is True:
+ if prop == 'stroke':
+ if val == 'none':
+ new_val = '#000000'
+ declarations[i] = prop + ':' + new_val
+ if self.options.harmonize_colors is True:
+ if prop == 'fill':
+ if re.search('fill:none(.*?)(;|$)', style) is None:
+ rgb = inkex.Color(val).to_rgb()
+ closest_color = self.closestColor(self.roundUpColors, [rgb[0], rgb[1], rgb[2]])
+ rgbNew = inkex.Color((
+ int(closest_color[0][0]),
+ int(closest_color[0][1]),
+ int(closest_color[0][2])
+ ), space='rgb')
+ declarations[i] = prop + ':' + str(inkex.Color(rgbNew).to_named())
+ if prop == 'stroke':
+ if re.search('stroke:none(.*?)(;|$)', style) is None:
+ rgb = inkex.Color(val).to_rgb()
+ closest_color = self.closestColor(self.roundUpColors, [rgb[0], rgb[1], rgb[2]])
+ rgbNew = inkex.Color((
+ int(closest_color[0][0]),
+ int(closest_color[0][1]),
+ int(closest_color[0][2])
+ ), space='rgb')
+ declarations[i] = prop + ':' + str(inkex.Color(rgbNew).to_named())
+ if self.options.reset_fill_attributes is True:
+ if prop == 'fill':
+ new_val = 'none'
+ declarations[i] = prop + ':' + new_val
+ if prop == 'fill-opacity':
+ new_val = '1'
+ declarations[i] = prop + ':' + new_val
+ if self.options.apply_hairlines is False:
+ if prop == '-inkscape-stroke':
+ if val == 'hairline':
+ del declarations[i]
+ if prop == 'vector-effect':
+ if val == 'non-scaling-stroke':
+ del declarations[i]
+ node.set('style', ';'.join(declarations))
+
+ # if element is group we add it to collection to remove it's style after parsing all selected items
+ elif isinstance(node, inkex.ShapeElement) and isinstance(node, inkex.Group):
+ self.groups.append(node)
+
+if __name__ == '__main__':
+ CleanupStyles().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/cleanup_styles/meta.json b/extensions/fablabchemnitz/cleanup_styles/meta.json
new file mode 100644
index 0000000..bb94f6b
--- /dev/null
+++ b/extensions/fablabchemnitz/cleanup_styles/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Cleanup Styles",
+ "id": "fablabchemnitz.de.cleanup_styles",
+ "path": "cleanup_styles",
+ "dependent_extensions": null,
+ "original_name": "Cleanup",
+ "original_id": "com.attoparsec.filter.cleanup",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/attoparsec/inkscape-extensions/blob/master/cleanup.py",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/cleanup_styles",
+ "fork_url": "https://github.com/attoparsec/inkscape-extensions",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Cleanup+Styles",
+ "inkscape_gallery_url": "https://inkscape.org/de/~MarioVoigt/%E2%98%85cleanup-styles",
+ "main_authors": [
+ "github.com/attoparsec",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/color_harmony/.gitignore b/extensions/fablabchemnitz/color_harmony/.gitignore
new file mode 100644
index 0000000..cd4c22c
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/.gitignore
@@ -0,0 +1 @@
+*__pycache__*
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony.inx b/extensions/fablabchemnitz/color_harmony/color_harmony.inx
new file mode 100644
index 0000000..1a96659
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony.inx
@@ -0,0 +1,87 @@
+
+
+ Color Harmony
+ fablabchemnitz.de.color_harmony
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 50
+
+
+
+
+
+
+
+
+
+ false
+ false
+ false
+
+
+ false
+ false
+ false
+
+
+ false
+ false
+ false
+
+
+ false
+
+
+ 0.1
+
+ 10
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+ My Palette
+
+
+
+
+
+
+ all
+
+
+
+
+
+ Generate color harmonies and save as palette file
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony.py b/extensions/fablabchemnitz/color_harmony/color_harmony.py
new file mode 100644
index 0000000..14bfcad
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+
+# Color Harmony - Inkscape extension to generate
+# palettes of colors that go well together
+#
+# Version 0.2 "golem"
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+
+"""
+This extension allows you to automatically add guides to your Inkscape documents.
+"""
+
+#from math import sqrt
+#import types
+import inkex
+from inkex import Group, Rectangle
+from inkex.colors import is_color
+from color_harmony.colorplus import ColorPlus
+from color_harmony.harmonies import *
+from color_harmony.shades import *
+
+
+class ColorHarmony(inkex.EffectExtension):
+ """Generate palettes of colors that go well together"""
+
+ color = ''
+
+ def add_arguments(self, pars):
+ # General options
+ pars.add_argument('--tab', default='render', help='Extension functionality to use') # options: render, save, colorize
+
+ # Render tab options
+ pars.add_argument('--harmony', default="five", help='Color harmony to generate, options: from_raster, just_opposite, split_complementary, three, four, rectangle, five, similar_3, similar_5, similar_and_opposite')
+ pars.add_argument('--sort', default="by_hue", help="Method to sort the palette by, options: by_hue, by_saturation, by_value, hue_contiguous")
+ pars.add_argument('--factor', default=50, type=int, help="Factor to affect the result, between 1 and 100. Default is 50. This modifies the angle between the resulting colors on the color wheel.")
+ pars.add_argument('--size', default=10, help="Size of the generated palette squares")
+ pars.add_argument('--unit', default='mm', help='Units') # options: mm, cm, in, px, pt, pc
+ pars.add_argument('--delete_existing', type=inkex.Boolean, help='Delete existing palettes before generating a new one')
+
+ # Shading: cooler, warmer, saturation, value, chroma, luma, hue, hue_luma, luma_plus_chroma, luma_minus_chroma
+ pars.add_argument( '--cooler', type=inkex.Boolean, help='Add shades with cooler color temperature')
+ pars.add_argument( '--warmer', type=inkex.Boolean, help='Add shades with warmer color temperature')
+ pars.add_argument( '--saturation', type=inkex.Boolean, help='Add shades with saturation steps')
+ pars.add_argument( '--value', type=inkex.Boolean, help='Add shades with value steps')
+ pars.add_argument( '--chroma', type=inkex.Boolean, help='Add shades with chroma steps')
+ pars.add_argument( '--luma', type=inkex.Boolean, help='Add shades with luma steps')
+ pars.add_argument( '--hue', type=inkex.Boolean, help='Add shades with hue steps')
+ pars.add_argument( '--hue_luma', type=inkex.Boolean, help='Add shades with hue and luma steps')
+ pars.add_argument( '--luma_plus_chroma', type=inkex.Boolean, help='Add shades with luma plus chroma steps')
+ pars.add_argument( '--luma_minus_chroma', type=inkex.Boolean, help='Add shades with luma minus chroma steps')
+ pars.add_argument('--step_width', type=float, default=0.1, help='Shader step width') # TODO: find out what range this can take on, and adjust min, max, default in inx
+
+ # Save tab options
+ pars.add_argument('--palette_format', default='gimp', help='Palette file format')
+ # options: gimp, krita, scribus
+ pars.add_argument('--palette_folder', help="Folder to save the palette file in")
+ pars.add_argument('--palette_name', help="Name of the palette")
+
+ # Colorize tab options
+ # no options currently
+
+
+ def effect(self):
+
+ if self.options.tab == "render":
+
+ if len(self.svg.selected) == 1:
+ for obj_id, obj in self.svg.selected.items():
+ fill = obj.style.get("fill")
+ if is_color(fill):
+ if self.options.delete_existing:
+ self.delete_existing_palettes()
+ self.color = ColorPlus(fill)
+ self.options.factor = self.options.factor/100
+ colors = self.create_harmony()
+ shades = self.create_shades(colors)
+ palettes = [colors] + shades
+
+ for i in range(len(palettes)):
+ self.render_palette(palettes[i], i)
+ else:
+ raise inkex.AbortExtension(
+ "Please select an object with a plain fill color.")
+ else:
+ raise inkex.AbortExtension(
+ "Please select one object.")
+ elif self.options.tab == "save":
+ palettes = self.get_palettes_in_doc()
+ if len(palettes) >= 1:
+ self.save_palette(palettes[0])
+ else:
+ raise inkex.AbortExtension(
+ "There is no rendered palette in the document. Please render a palette using the first tab of the dialog before you try to save it.")
+ elif self.options.tab == "colorize":
+ if len(self.svg.selected) > 0:
+ self.colorize()
+ else:
+ raise inkex.AbortExtension(
+ "Please select an object to colorize!")
+
+
+ # METHODS FOR EACH TAB
+ # --------------------
+
+ # Render tab
+ # ==========
+ def create_harmony(self):
+
+ harmony_functions = {
+ "from_raster": self.palette_from_raster, # not implemented yet
+ "just_opposite": self.opposite,
+ "split_complementary": self.splitcomplementary,
+ "three": self.nhues3,
+ "four": self.nhues4,
+ "rectangle": self.rectangle,
+ "five": self.fivecolors,
+ "similar_3": self.similar_3,
+ "similar_5": self.similar_5,
+ "similar_and_opposite": self.similaropposite,
+ }
+
+ # use appropriate function for the selected tab
+ colors = harmony_functions[self.options.harmony](self.color)
+ colors = self.sort_colors(colors)
+
+ return colors
+
+ def render_palette(self, colors, shift):
+ size = self.svg.unittouu(str(self.options.size)+self.options.unit)
+ top = 0 + shift * size
+ left = 0
+
+ layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
+
+ group_attribs = {inkex.addNS('label', 'inkscape'): "Palette ({harmony}, {color}) ".format(color=self.color, harmony=self.options.harmony)}
+ palette_group = Group(**group_attribs)
+
+ for color in colors:
+ palette_field = Rectangle(x=str(left),
+ y=str(top),
+ width=str(size),
+ height=str(size))
+ palette_field.style = {'fill': color}
+ palette_group.add(palette_field)
+ left += size
+
+ palette_group.transform.add_translate(0, self.svg.viewport_height + size)
+
+ layer.add(palette_group)
+
+ def palette_from_raster(self, color):
+ # TODO: implement
+ return []
+
+ def opposite(self, color):
+ colors = opposite(color)
+ return colors
+
+ def splitcomplementary(self, color):
+ colors = splitcomplementary(color, self.options.factor)
+ return colors
+
+ def nhues3(self, color):
+ colors = nHues(color, 3)
+ return colors
+
+ def nhues4(self, color):
+ colors = nHues(color, 4)
+ return colors
+
+ def rectangle(self, color):
+ colors = rectangle(color, self.options.factor)
+ return colors
+
+ def fivecolors(self, color):
+ colors = fiveColors(color, self.options.factor)
+ return colors
+
+ def similar_3(self, color):
+ colors = similar(color, 3, self.options.factor)
+ return colors
+
+ def similar_5(self, color):
+ colors = similar(color, 5, self.options.factor)
+ return colors
+
+ def similaropposite(self, color):
+ colors = similarAndOpposite(color, self.options.factor)
+ return colors
+
+ def create_shades(self, colors):
+ shades = []
+ shading_options = {
+ "cooler": cooler,
+ "warmer": warmer,
+ "saturation": saturation,
+ "value": value,
+ "chroma": chroma,
+ "luma": luma,
+ "hue": hue,
+ "hue_luma": hue_luma,
+ "luma_plus_chroma": luma_plus_chroma,
+ "luma_minus_chroma": luma_minus_chroma,
+ }
+
+ for option, function in shading_options.items():
+ if vars(self.options)[option] == True:
+ # shades are created per color,
+ # but we want to get one palette per shading step
+ shaded_colors = []
+ for i in range(len(colors)):
+ shaded_colors.append(function(colors[i], self.options.step_width))
+
+ pals = [list(a) for a in zip(*shaded_colors)]
+
+ shades += pals
+ return shades
+
+ def delete_existing_palettes(self):
+ """Delete all palettes in the document"""
+
+ for palette in self.get_palettes_in_doc():
+ palette.delete()
+
+ # Save tab
+ # ========
+ def save_palette(self, palette):
+ # TODO: implement
+ # if not hasattr(self.palette, 'name'):
+ # if type(file_w) in [str, unicode]:
+ # self.palette.name = basename(file_w)
+ # else:
+ # self.palette.name='Colors'
+ pass
+
+
+ # Colorize tab
+ # ============
+
+ def colorize(self):
+ # TODO: implement
+ pass
+
+ # HELPER FUNCTIONS
+ # ----------------
+
+ def get_palettes_in_doc(self):
+ palettes = []
+ for group in self.svg.findall('.//svg:g'):
+ if group.get('inkscape:label').startswith('Palette ('):
+ palettes.append(group)
+ return palettes
+
+ def sort_colors(self, colors):
+ if self.options.sort == "by_hue":
+ colors.sort(key=lambda color: color.to_hsv()[0])
+ elif self.options.sort == "by_saturation":
+ colors.sort(key=lambda color: color.to_hsv()[1])
+ elif self.options.sort == "by_value":
+ colors.sort(key=lambda color: color.to_hsv()[2])
+ # this option looks nicer when the palette colors are similar red tones
+ # some of which have a hue close to 0
+ # and some of which have a hue close to 1
+ elif self.options.sort == "hue_contiguous":
+ # sort by hue first
+ colors.sort(key=lambda color: color.to_hsv()[0])
+ # now find out if the hues are maybe clustered around the 0 - 1 boundary
+ hues = [color.to_hsv()[0] for color in colors]
+ start_hue = 0
+ end_hue = 0
+ max_dist = 0
+ for i in range(len(colors)-1):
+ h1 = hues[i]
+ h2 = hues[i+1]
+ cur_dist = h2-h1
+ if cur_dist > max_dist and self.no_colors_in_between(h1, h2, hues):
+ max_dist = cur_dist
+ start_hue = h2
+ for i in range(len(colors)):
+ sorting_hue = hues[i] - start_hue
+ if sorting_hue > 1:
+ sorting_hue -=1
+ elif sorting_hue < 0:
+ sorting_hue += 1
+ hues[i] = sorting_hue
+ sorted_colors = [color for hue, color in sorted(zip(hues,colors))]
+ colors = sorted_colors
+ else:
+ raise inkex.AbortExtension(
+ "Please select one of the following sorting options: by_hue, by_saturation, by_value.")
+ return colors
+
+ def no_colors_in_between(self, hue1, hue2, hues):
+ for hue in hues:
+ if hue > hue1 and hue < hue2:
+ return False
+ return True
+
+if __name__ == '__main__':
+ ColorHarmony().run()
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/__init__.py b/extensions/fablabchemnitz/color_harmony/color_harmony/__init__.py
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/__init__.py
@@ -0,0 +1 @@
+# empty
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/colorplus.py b/extensions/fablabchemnitz/color_harmony/color_harmony/colorplus.py
new file mode 100644
index 0000000..d9579c7
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/colorplus.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+
+from math import cos, acos, sqrt, pi
+import colorsys
+from inkex.colors import Color
+
+_HCY_RED_LUMA = 0.299
+_HCY_GREEN_LUMA = 0.587
+_HCY_BLUE_LUMA = 0.114
+
+class ColorPlus(Color):
+
+ #HCYwts = 0.299, 0.587, 0.114
+
+ ## HCY colour space.
+ #
+ # Copy&Paste from https://raw.githubusercontent.com/mypaint/mypaint/master/gui/colors/uicolor.py
+ # Copyright (C) 2012-2013 by Andrew Chadwick
+ #
+
+ # Frequently referred to as HSY, Hue/Chroma/Luma, HsY, HSI etc. It can be
+ # thought of as a cylindrical remapping of the YCbCr solid: the "C" term is the
+ # proportion of the maximum permissible chroma within the RGB gamut at a given
+ # hue and luma. Planes of constant Y are equiluminant.
+ #
+ # ref https://code.google.com/p/colour-space-viewer/
+ # ref git://anongit.kde.org/kdelibs in kdeui/colors/kcolorspaces.cpp
+ # ref http://blog.publicfields.net/2011/12/rgb-hue-saturation-luma.html
+ # ref Joblove G.H., Greenberg D., Color spaces for computer graphics.
+ # ref http://www.cs.rit.edu/~ncs/color/t_convert.html
+ # ref http://en.literateprograms.org/RGB_to_HSV_color_space_conversion_(C)
+ # ref http://lodev.org/cgtutor/color.html
+ # ref Levkowitz H., Herman G.T., "GLHS: a generalized lightness, hue, and
+ # saturation color model"
+
+ # For consistency, use the same weights that the Color and Luminosity layer
+ # blend modes use, as also used by brushlib's Colorize brush blend mode. We
+ # follow http://www.w3.org/TR/compositing/ here. BT.601 YCbCr has a nearly
+ # identical definition of luma.
+
+ def __init__(self, color=None, space='rgb'):
+ super().__init__(color)
+
+ def to_hcy(self):
+ """RGB → HCY: R,G,B,H,C,Y ∈ [0, 1]
+
+ :param rgb: Color expressed as an additive RGB triple.
+ :type rgb: tuple (r, g, b) where 0≤r≤1, 0≤g≤1, 0≤b≤1.
+ :rtype: tuple (h, c, y) where 0≤h<1, but 0≤c≤2 and 0≤y≤1.
+
+ """
+ r, g, b = self.to_floats()
+
+ # Luma is just a weighted sum of the three components.
+ y = _HCY_RED_LUMA*r + _HCY_GREEN_LUMA*g + _HCY_BLUE_LUMA*b
+
+ # Hue. First pick a sector based on the greatest RGB component, then add
+ # the scaled difference of the other two RGB components.
+ p = max(r, g, b)
+ n = min(r, g, b)
+ d = p - n # An absolute measure of chroma: only used for scaling.
+ if n == p:
+ h = 0.0
+ elif p == r:
+ h = (g - b)/d
+ if h < 0:
+ h += 6.0
+ elif p == g:
+ h = ((b - r)/d) + 2.0
+ else: # p==b
+ h = ((r - g)/d) + 4.0
+ h /= 6.0
+
+ # Chroma, relative to the RGB gamut envelope.
+ if r == g == b:
+ # Avoid a division by zero for the achromatic case.
+ c = 0.0
+ else:
+ # For the derivation, see the GLHS paper.
+ c = max((y-n)/y, (p-y)/(1-y))
+ return h, c, y
+
+ @staticmethod
+ def from_hcy(h, c, y):
+ """HCY → RGB: R,G,B,H,C,Y ∈ [0, 1]
+
+ :param hcy: Color expressed as a Hue/relative-Chroma/Luma triple.
+ :type hcy: tuple (h, c, y) where 0≤h<1, but 0≤c≤2 and 0≤y≤1.
+ :rtype: ColorPlus object.
+
+ >>> n = 32
+ >>> diffs = [sum( [abs(c1-c2) for c1, c2 in
+ ... zip( HCY_to_RGB(RGB_to_HCY([r/n, g/n, b/n])),
+ ... [r/n, g/n, b/n] ) ] )
+ ... for r in range(int(n+1))
+ ... for g in range(int(n+1))
+ ... for b in range(int(n+1))]
+ >>> sum(diffs) < n*1e-6
+ True
+
+ """
+
+ if c == 0:
+ return y, y, y
+
+ h %= 1.0
+ h *= 6.0
+ if h < 1:
+ #implies (p==r and h==(g-b)/d and g>=b)
+ th = h
+ tm = _HCY_RED_LUMA + _HCY_GREEN_LUMA * th
+ elif h < 2:
+ #implies (p==g and h==((b-r)/d)+2.0 and b=g)
+ th = h - 2.0
+ tm = _HCY_GREEN_LUMA + _HCY_BLUE_LUMA * th
+ elif h < 4:
+ #implies (p==b and h==((r-g)/d)+4.0 and r=g)
+ th = h - 4.0
+ tm = _HCY_BLUE_LUMA + _HCY_RED_LUMA * th
+ else:
+ #implies (p==r and h==(g-b)/d and g= y:
+ p = y + y*c*(1-tm)/tm
+ o = y + y*c*(th-tm)/tm
+ n = y - (y*c)
+ else:
+ p = y + (1-y)*c
+ o = y + (1-y)*c*(th-tm)/(1-tm)
+ n = y - (1-y)*c*tm/(1-tm)
+
+ # Back to RGB order
+ if h < 1: r, g, b = p, o, n
+ elif h < 2: r, g, b = o, p, n
+ elif h < 3: r, g, b = n, p, o
+ elif h < 4: r, g, b = n, o, p
+ elif h < 5: r, g, b = o, n, p
+ else: r, g, b = p, n, o
+
+ return ColorPlus([255*r, 255*g, 255*b])
+
+ def to_hsv(self):
+ r, g, b = self.to_floats()
+ eps = 0.001
+ if abs(max(r,g,b)) < eps:
+ return (0,0,0)
+ return colorsys.rgb_to_hsv(r, g, b)
+
+ @staticmethod
+ def from_hsv(h, s, v):
+ r, g, b = colorsys.hsv_to_rgb(h, s, v)
+ return ColorPlus([255*r, 255*g, 255*b])
+
+ # TODO: everything below is not updated yet, maybe not really needed
+ def hex(self):
+ r,g,b = self.getRGB()
+ return "#{:02x}{:02x}{:02x}".format(r,g,b)
+
+ def getRgbString(self):
+ r,g,b = self.getRGB()
+ return "rgb({}, {}, {})".format(r,g,b)
+
+ def getHsvString(self):
+ h,s,v = self.getHSV()
+ return "hsv({}, {}, {})".format(h,s,v)
+
+ def invert(self):
+ r, g, b = self._rgb
+ return Color(255-r, 255-g, 255-b)
+
+ def darker(clr, q):
+ h,s,v = clr.getHSV()
+ v = clip(v-q)
+ return hsv(h,s,v)
+
+ def lighter(clr, q):
+ h,s,v = clr.getHSV()
+ v = clip(v+q)
+ return hsv(h,s,v)
+
+ def saturate(clr, q):
+ h,s,v = clr.getHSV()
+ s = clip(s+q)
+ return hsv(h,s,v)
+
+ def desaturate(clr, q):
+ h,s,v = clr.getHSV()
+ s = clip(s-q)
+ return hsv(h,s,v)
+
+ def increment_hue(clr, q):
+ h,s,v = clr.getHSV()
+ h += q
+ if h > 1.0:
+ h -= 1.0
+ if h < 1.0:
+ h += 1.0
+ return hsv(h,s,v)
+
+ def contrast(clr, q):
+ h,s,v = clr.getHSV()
+ v = (v - 0.5)*(1.0 + q) + 0.5
+ v = clip(v)
+ return hsv(h,s,v)
+
+ def linear(x, y, q):
+ return (1.-q)*x + q*y
+
+ def linear3(v1, v2, q):
+ x1, y1, z1 = v1
+ x2, y2, z2 = v2
+ return (linear(x1, x2, q), linear(y1, y2, q), linear(z1, z2, q))
+
+ def circular(h1, h2, q, circle=1.0):
+ #print("Src hues: "+ str((h1, h2)))
+ d = h2 - h1
+ if h1 > h2:
+ h1, h2 = h2, h1
+ d = -d
+ q = 1.0 - q
+ if d > circle/2.0:
+ h1 = h1 + circle
+ h = linear(h1, h2, q)
+ else:
+ h = h1 + q*d
+ if h >= circle:
+ h -= circle
+ #print("Hue: "+str(h))
+ return h
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/export.py b/extensions/fablabchemnitz/color_harmony/color_harmony/export.py
new file mode 100644
index 0000000..a391d5f
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/export.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+
+from os.path import join, basename
+from lxml import etree as ET
+from zipfile import ZipFile, ZIP_DEFLATED
+
+
+class PaletteFile(object)
+
+ def __init__(self, name, colors, filename, folder):
+ self.colors = colors
+ if name != "":
+ self.name = name
+ else:
+ self.name = "Palette"
+ self.folder = folder
+
+ def save(self, format):
+ FORMATS = {'gimp': [build_gpl, 'gpl'],
+ 'scribus': [build_scribus_xml, 'xml'],
+ 'krita': [build_kpl, 'kpl'],
+ 'css': [build_css, 'css'],
+ 'android_xml': [build_android_xml, 'xml'],
+ }
+
+ if os.path.exists(self.folder):
+ # save with given file name
+ pass
+
+ def build_gpl(self):
+ # TODO: fix
+ palette_string = (u"Name: {}\n".format(self.palette.name).encode('utf-8'))
+ if hasattr(self.palette, 'ncols') and self.palette.ncols:
+ palette_string += 'Columns: %s\n' % self.palette.ncols
+ for key,value in self.palette.meta.items():
+ if key != "Name":
+ palette_string += u"# {}: {}\n".format(key, value).encode('utf-8')
+ palette_string += '#\n'
+ for row in self.palette.slots:
+ for slot in row:
+ n = slot.name
+ r, g, b = slot.color.getRGB()
+ s = '%d %d %d %s\n' % (r, g, b, n)
+ palette_string += s
+ for key,value in slot.color.meta.items():
+ if key != "Name":
+ palette_string += u"# {}: {}\n".format(key, value).encode('utf-8')
+ return palette_string
+
+ def build_kpl(self):
+ # TODO: fix (and don't save it here, only build)
+ with ZipFile(file_w, 'w', ZIP_DEFLATED) as zf:
+ zf.writestr("mimetype", MIMETYPE)
+
+ xml = ET.Element("Colorset")
+ xml.attrib['version'] = '1.0'
+ xml.attrib['columns'] = str(self.palette.ncols)
+ xml.attrib['name'] = self.palette.name
+ xml.attrib['comment'] = self.palette.meta.get("Comment", "Generated by Palette Editor")
+
+ for i,row in enumerate(self.palette.slots):
+ for j,slot in enumerate(row):
+ color = slot.color
+ name = color.name
+ default_name = "Swatch-{}-{}".format(i,j)
+ if not name:
+ name = default_name
+
+ elem = ET.SubElement(xml, "ColorSetEntry")
+ elem.attrib['spot'] = color.meta.get("Spot", "false")
+ elem.attrib['id'] = default_name
+ elem.attrib['name'] = name
+ elem.attrib['bitdepth'] = 'U8'
+
+ r,g,b = color.getRGB1()
+ srgb = ET.SubElement(elem, "sRGB")
+ srgb.attrib['r'] = str(r)
+ srgb.attrib['g'] = str(g)
+ srgb.attrib['b'] = str(b)
+
+ tree = ET.ElementTree(xml)
+ tree_str = ET.tostring(tree, encoding='utf-8', pretty_print=True, xml_declaration=False)
+
+ zf.writestr("colorset.xml", tree_str)
+
+ def build_scribus_xml(self):
+ # TODO: fix, and don't save here
+ xml = ET.Element("SCRIBUSCOLORS", NAME=name)
+
+ for i,row in enumerate(self.palette.getColors()):
+ for j,color in enumerate(row):
+ name = color.name
+ if not name:
+ name = "Swatch-{}-{}".format(i,j)
+ elem = ET.SubElement(xml, "COLOR", NAME=name, RGB=color.hex())
+ if "Spot" in color.meta:
+ elem.attrib["Spot"] = color.meta["Spot"]
+ if "Register" in color.meta:
+ elem.attrib["Register"] = color.meta["Register"]
+
+ ET.ElementTree(xml).write(file_w, encoding="utf-8", pretty_print=True, xml_declaration=True)
+
+ def build_android_xml(self):
+ # https://stackoverflow.com/questions/3769762/web-colors-in-an-android-color-xml-resource-file
+ palette_string = ''
+ # TODO: implement
+ return palette_string
+
+ def build_css(self):
+ palette_string = ''
+
+ # TODO: fix
+ for i, row in enumerate(self.palette.slots):
+ for j, slot in enumerate(row):
+ hex = slot.color.hex()
+ s = ".color-{}-{}{} {{ color: {} }};\n".format(i,j, user, hex)
+ palette_string += s
+
+ return palette_string
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/harmonies.py b/extensions/fablabchemnitz/color_harmony/color_harmony/harmonies.py
new file mode 100644
index 0000000..0f89680
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/harmonies.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+from math import sqrt, sin, cos
+from color_harmony.colorplus import ColorPlus
+from color_harmony.utils import seq, circle_hue
+
+
+# Each of these functions takes one ColorPlus object
+# and returns a list of ColorPlus objects:
+__all__ = ['opposite', 'splitcomplementary', 'similar', 'similarAndOpposite', 'rectangle', 'nHues', 'fiveColors']
+
+# Harmony functions
+# 180° rotation
+def opposite(color):
+ h, s, v = color.to_hsv()
+ h = h + 0.5
+ if h > 1.0:
+ h -= 1.0
+ return [color, ColorPlus.from_hsv(h, s, v)]
+
+# default value 0.5 corresponds to +-36° deviation from opposite, max. useful value: 89°, min. useful value: 1°
+def splitcomplementary(color, parameter=0.5):
+ h, s, v = color.to_hsv()
+ h += (1.0 - 0.4*parameter)/2.0
+ if h > 1.0:
+ h -= 1.0
+ c1 = ColorPlus.from_hsv(h,s,v)
+ h += 0.4*parameter
+ if h > 1.0:
+ h -= 1.0
+ c2 = ColorPlus.from_hsv(h,s,v)
+ return [color, c1, c2]
+
+# default value 0.5 corresponds to 36° per step, max. useful value: 360°/n , min. useful value: 1°
+def similar(color, n, parameter=0.5):
+ h, s, v = color.to_hsv()
+ step = 0.2 * parameter
+ hmin = h - (n // 2) * step
+ hmax = h + (n // 2) * step
+ return [ColorPlus.from_hsv(dh % 1.0, s, v) for dh in seq(hmin, hmax, step)]
+
+# default value 0.5 corresponds to 36° deviation from original, max. useful value: 178°, min. useful value: 1°
+def similarAndOpposite(color, parameter=0.5):
+ h, c, y = color.to_hcy()
+ h1 = h + 0.2 * parameter
+ if h1 > 1.0:
+ h1 -= 1.0
+ h2 = h - 0.2 * parameter
+ if h2 < 0.0:
+ h2 += 1.0
+ h3 = h + 0.5
+ if h3 > 1.0:
+ h3 -= 1.0
+ return [ColorPlus.from_hcy(h1,c,y),
+ color,
+ ColorPlus.from_hcy(h2,c,y),
+ ColorPlus.from_hcy(h3,c,y)]
+
+# default value 0.5 corresponds to 36° deviation from original, max. useful angle 180°, min. useful angle 1°
+def rectangle(color, parameter=0.5):
+ h, c, y = color.to_hcy()
+ h1 = (h + 0.2 * parameter) % 1.0
+ h2 = (h1 + 0.5) % 1.0
+ h3 = (h + 0.5) % 1.0
+ return [color,
+ ColorPlus.from_hcy(h1,c,y),
+ ColorPlus.from_hcy(h2,c,y),
+ ColorPlus.from_hcy(h3,c,y)]
+
+# returns n colors that are placed on the hue circle in steps of 360°/n
+def nHues(color, n):
+ h, s, v = color.to_hsv()
+ return [color] + [ColorPlus.from_hsv(circle_hue(i, n, h), s, v) for i in range(1, n)]
+
+# parameter determines +/- deviation from a the hues +/-120° away, default value 0.5 corresponds to 2.16°, max. possible angle 4.32°
+def fiveColors(color, parameter=0.5):
+ h0, s, v = color.to_hsv()
+ h1s = (h0 + 1.0/3.0) % 1.0
+ h2s = (h1s + 1.0/3.0) % 1.0
+ delta = 0.06 * parameter
+ h1 = (h1s - delta) % 1.0
+ h2 = (h1s + delta) % 1.0
+ h3 = (h2s - delta) % 1.0
+ h4 = (h2s + delta) % 1.0
+ return [color] + [ColorPlus.from_hsv(h,s,v) for h in [h1,h2,h3,h4]]
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/matching/__init__.py b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/__init__.py
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/__init__.py
@@ -0,0 +1 @@
+# empty
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/matching/matching.py b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/matching.py
new file mode 100644
index 0000000..44745f0
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/matching.py
@@ -0,0 +1,117 @@
+
+from color.colors import *
+from color.spaces import *
+
+def find_min(idx, occupied, d):
+ best_i = None
+ best_y = None
+ best_clr = None
+ for i, clr in d.iteritems():
+ if i in occupied:
+ continue
+ y = clr[idx]
+ if best_y is None or y < best_y:
+ best_i = i
+ best_y = y
+ best_clr = clr
+ if best_y is not None:
+ return best_i
+ for i, clr in d.iteritems():
+ y = clr[idx]
+ if best_y is None or y < best_y:
+ best_i = i
+ best_y = y
+ best_clr = clr
+ assert best_i is not None
+ return best_i
+
+def find_max(idx, occupied, d):
+ best_i = None
+ best_y = None
+ best_clr = None
+ for i, clr in d.iteritems():
+ if i in occupied:
+ continue
+ y = clr[idx]
+ if best_y is None or y > best_y:
+ best_i = i
+ best_y = y
+ best_clr = clr
+ if best_y is not None:
+ return best_i
+ for i, clr in d.iteritems():
+ y = clr[idx]
+ if best_y is None or y > best_y:
+ best_i = i
+ best_y = y
+ best_clr = clr
+ assert best_i is not None
+ return best_i
+
+def match_colors(colors1, colors2):
+ hsvs1 = dict(enumerate([c.getHCY() for c in colors1 if c is not None]))
+ hsvs2 = dict(enumerate([c.getHCY() for c in colors2 if c is not None]))
+ occupied = []
+ result = {}
+ while len(hsvs1.keys()) > 0:
+ # Darkest of SVG colors
+ darkest1_i = find_min(2, [], hsvs1)
+ # Darkest of palette colors
+ darkest2_i = find_min(2, occupied, hsvs2)
+ hsvs1.pop(darkest1_i)
+ occupied.append(darkest2_i)
+ result[darkest1_i] = darkest2_i
+ if not hsvs1:
+ break
+
+ # Lightest of SVG colors
+ lightest1_i = find_max(2, [], hsvs1)
+ # Lightest of palette colors
+ lightest2_i = find_max(2, occupied, hsvs2)
+ hsvs1.pop(lightest1_i)
+ occupied.append(lightest2_i)
+ result[lightest1_i] = lightest2_i
+ if not hsvs1:
+ break
+
+ # Less saturated of SVG colors
+ grayest1_i = find_min(1, [], hsvs1)
+ # Less saturated of palette colors
+ grayest2_i = find_min(1, occupied, hsvs2)
+ hsvs1.pop(grayest1_i)
+ occupied.append(grayest2_i)
+ result[grayest1_i] = grayest2_i
+ if not hsvs1:
+ break
+
+ # Most saturated of SVG colors
+ saturated1_i = find_max(1, [], hsvs1)
+ # Most saturated of palette colors
+ saturated2_i = find_max(1, occupied, hsvs2)
+ hsvs1.pop(saturated1_i)
+ occupied.append(saturated2_i)
+ result[saturated1_i] = saturated2_i
+ if not hsvs1:
+ break
+
+ redest1_i = find_min(0, [], hsvs1)
+ redest2_i = find_min(0, occupied, hsvs2)
+ hsvs1.pop(redest1_i)
+ occupied.append(redest2_i)
+ result[redest1_i] = redest2_i
+ if not hsvs1:
+ break
+
+ bluest1_i = find_max(0, [], hsvs1)
+ bluest2_i = find_max(0, occupied, hsvs2)
+ hsvs1.pop(bluest1_i)
+ occupied.append(bluest2_i)
+ result[bluest1_i] = bluest2_i
+
+ clrs = []
+ for i in range(len(result.keys())):
+ j = result[i]
+ clrs.append(colors2[j])
+ return clrs
+
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg.py b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg.py
new file mode 100644
index 0000000..b57d604
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg.py
@@ -0,0 +1,96 @@
+
+import re
+from lxml import etree
+
+from color import colors
+
+SVG_NS="http://www.w3.org/2000/svg"
+
+color_re = re.compile("#[0-9a-fA-F]+")
+
+def walk(processor, element):
+ for child in element.iter():
+ processor(child)
+
+class Collector(object):
+ def __init__(self):
+ self.colors = {}
+ self.n = 0
+
+ def _parse(self, string):
+ xs = string.split(";")
+ single = len(xs) == 1
+
+ result = {}
+ for x in xs:
+ ts = x.split(":")
+ if len(ts) < 2:
+ if single:
+ return None
+ else:
+ continue
+ key, value = ts[0], ts[1]
+ result[key] = value
+ return result
+
+ def _merge(self, attr):
+ if type(attr) == str:
+ return attr
+ result = ""
+ for key in attr:
+ value = attr[key]
+ result += key + ":" + value + ";"
+ return result
+
+ def _is_color(self, val):
+ return color_re.match(val) is not None
+
+ def _remember_color(self, color):
+ if color not in self.colors:
+ self.colors[color] = self.n
+ self.n += 1
+ n = self.colors[color]
+ return "${color%s}" % n
+
+ def _process_attr(self, value):
+ d = self._parse(value)
+ if d is None:
+ if self._is_color(value):
+ return self._remember_color(value)
+ else:
+ return value
+ elif type(d) == dict:
+ for attr in ['fill', 'stroke', 'stop-color']:
+ if (attr in d) and self._is_color(d[attr]):
+ color = d[attr]
+ d[attr] = self._remember_color(color)
+ return self._merge(d)
+ else:
+ if self._is_color(value):
+ return self._remember_color(value)
+ else:
+ return value
+
+ def process(self, element):
+ for attr in ['fill', 'stroke', 'style', 'pagecolor', 'bordercolor']:
+ if attr in element.attrib:
+ value = element.get(attr)
+ element.set(attr, self._process_attr(value))
+
+ def result(self):
+ return self.colors
+
+def read_template(filename):
+ xml = etree.parse(filename)
+ collector = Collector()
+ walk(collector.process, xml.getroot())
+ svg = etree.tostring(xml, encoding='utf-8', xml_declaration=True, pretty_print=True)
+ #open("last_template.svg",'w').write(svg)
+ color_dict = collector.result()
+ colors_inv = dict((v,k) for k, v in color_dict.iteritems())
+ svg_colors = []
+ for key in range(len(colors_inv.keys())):
+ clr = colors_inv[key]
+ svg_colors.append( colors.fromHex(clr) )
+ return svg_colors, svg
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg_widget.py b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg_widget.py
new file mode 100644
index 0000000..321aa28
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/svg_widget.py
@@ -0,0 +1,135 @@
+
+from string import Template
+from PyQt4 import QtGui, QtSvg, QtCore
+
+from color.colors import *
+from color.spaces import *
+import svg, transform, matching
+
+class SvgTemplateWidget(QtSvg.QSvgWidget):
+ template_loaded = QtCore.pyqtSignal()
+ colors_matched = QtCore.pyqtSignal()
+ file_dropped = QtCore.pyqtSignal(unicode)
+
+ def __init__(self, *args):
+ QtSvg.QSvgWidget.__init__(self, *args)
+ self.setAcceptDrops(True)
+ self._colors = [Color(i*10,i*10,i*10) for i in range(20)]
+ self._template = None
+ self._template_filename = None
+ self._svg = None
+ self._need_render = True
+ self._svg_colors = None
+ self._dst_colors = None
+ self._last_size = None
+
+ def sizeHint(self):
+ if self.renderer().isValid():
+ return self.renderer().defaultSize()
+ elif self._last_size:
+ return self._last_size
+ else:
+ return QtCore.QSize(300,300)
+
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasUrls():
+ event.acceptProposedAction()
+
+ def dropEvent(self, event):
+ if event.mimeData().hasUrls():
+ urls = event.mimeData().urls()
+ path = unicode( urls[0].path() )
+ self.file_dropped.emit(path)
+
+ def _get_color(self, i):
+ if i < len(self._colors):
+ return self._colors[i]
+ else:
+ return Color(i*10, i*10, i*10)
+
+ def _update(self):
+ arr = QtCore.QByteArray.fromRawData(self.get_svg())
+ print("Data loaded: {} bytes".format(arr.length()))
+ self.load(arr)
+ if self.renderer().isValid():
+ self._last_size = self.renderer().defaultSize()
+ self.update()
+
+ def _get_image(self):
+ w,h = self.size().width(), self.size().height()
+ image = QtGui.QImage(w, h, QtGui.QImage.Format_ARGB32_Premultiplied)
+ image.fill(0)
+ qp = QtGui.QPainter()
+ qp.begin(image)
+ self.renderer().render(qp, QtCore.QRectF(0.0, 0.0, w, h))
+ qp.end()
+ return image
+
+ def loadTemplate(self, filename):
+ self._template_filename = filename
+ self._svg_colors, self._template = svg.read_template(filename)
+ print("Source SVG colors:")
+ for c in self._svg_colors:
+ print str(c)
+ print("Template loaded: {}: {} bytes".format(filename, len(self._template)))
+ self._need_render = True
+ self._update()
+ self.template_loaded.emit()
+
+ def set_color(self, idx, color):
+ self._colors[idx] = color
+ self._need_render = True
+ self._update()
+
+ def setColors(self, dst_colors, space=HCY):
+ if not dst_colors:
+ return
+ print("Matching colors in space: {}".format(space))
+ self._dst_colors = dst_colors
+ self._colors = transform.match_colors(space, self._svg_colors, dst_colors)
+ #self._colors = matching.match_colors(self._svg_colors, dst_colors)
+ self._need_render = True
+ self._update()
+ self.colors_matched.emit()
+
+ def resetColors(self):
+ self.load(self._template_filename)
+ self.repaint()
+
+ def get_svg_colors(self):
+ return self._svg_colors
+
+ def get_dst_colors(self):
+ return self._colors
+
+ def get_svg(self):
+ if self._svg is not None and not self._need_render:
+ return self._svg
+ else:
+ self._svg = self._render()
+ self._need_render = False
+ return self._svg
+
+ def _render(self):
+ #d = dict([("color"+str(i), color.hex() if color is not None else Color(255,255,255)) for i, color in enumerate(self._colors)])
+ d = ColorDict(self._colors)
+ #self._image = self._get_image()
+ return Template(self._template).substitute(d)
+
+class ColorDict(object):
+ def __init__(self, colors):
+ self._colors = colors
+
+ def __getitem__(self, key):
+ if key.startswith("color"):
+ n = int( key[5:] )
+ if n < len(self._colors):
+ return self._colors[n].hex()
+ else:
+ return "#ffffff"
+ else:
+ raise KeyError(key)
+
+
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/matching/transform.py b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/transform.py
new file mode 100644
index 0000000..55a55e9
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/transform.py
@@ -0,0 +1,241 @@
+#!/usr/bin/python3
+
+# -y11+a13*x13+a12*x12+a11*x11+b1
+# -y12+a23*x13+a22*x12+a21*x11+b2
+# -y13+a33*x13+a32*x12+a31*x11+b3
+# -y21+a13*x23+a12*x22+a11*x21+b1
+# -y22+a23*x23+a22*x22+a21*x21+b2
+# -y23+a33*x23+a32*x22+a31*x21+b3
+# -y31+a13*x33+a12*x32+a11*x31+b1
+# -y32+a23*x33+a22*x32+a21*x31+b2
+# -y33+a33*x33+a32*x32+a31*x31+b3
+# -y41+a13*x43+a12*x42+a11*x41+b1
+# -y42+a23*x43+a22*x42+a21*x41+b2
+# -y43+a33*x43+a32*x42+a31*x41+b3
+
+from math import sqrt
+import itertools
+import numpy as np
+from numpy.linalg import solve, det
+from numpy.linalg.linalg import LinAlgError
+from copy import deepcopy as copy
+from PyQt4 import QtGui
+
+from color.colors import *
+from color.spaces import *
+
+def get_A(x):
+ return np.array([[x[0][0], x[0][1], x[0][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, x[0][0], x[0][1], x[0][2], 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, x[0][0], x[0][1], x[0][2], 0, 0, 1],
+ [x[1][0], x[1][1], x[1][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, x[1][0], x[1][1], x[1][2], 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, x[1][0], x[1][1], x[1][2], 0, 0, 1],
+ [x[2][0], x[2][1], x[2][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, x[2][0], x[2][1], x[2][2], 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, x[2][0], x[2][1], x[2][2], 0, 0, 1],
+ [x[3][0], x[3][1], x[3][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
+ [0, 0, 0, x[3][0], x[3][1], x[3][2], 0, 0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0, 0, x[3][0], x[3][1], x[3][2], 0, 0, 1] ])
+
+def get_B(y):
+ return np.array([[y[0][0]], [y[0][1]], [y[0][2]],
+ [y[1][0]], [y[1][1]], [y[1][2]],
+ [y[2][0]], [y[2][1]], [y[2][2]],
+ [y[3][0]], [y[3][1]], [y[3][2]] ])
+
+def color_row(space, color):
+ x1,x2,x3 = space.getCoords(color)
+ return np.array([x1,x2,x3])
+
+def color_column(space, color):
+ x1,x2,x3 = space.getCoords(color)
+ return np.array([[x1],[x2],[x3]])
+
+def colors_array(space, *colors):
+ return np.array([color_row(space, c) for c in colors])
+
+def find_transform_colors(space, cx1, cx2, cx3, cx4, cy1, cy2, cy3, cy4):
+ x = colors_array(space, cx1, cx2, cx3, cx4)
+ y = colors_array(space, cy1, cy2, cy3, cy4)
+ return find_transform(x,y)
+
+def find_transform(x, y):
+ #print("X:\n"+str(x))
+ #print("Y:\n"+str(y))
+ m = solve(get_A(x), get_B(y))
+ a = np.array([[m[0][0], m[1][0], m[2][0]],
+ [m[3][0], m[4][0], m[5][0]],
+ [m[6][0], m[7][0], m[8][0]]])
+ b = np.array([[m[9][0]], [m[10][0]], [m[11][0]]])
+ return (a, b)
+
+def transform_colors(space, a, b, cx):
+ x = color_column(space, cx)
+ y = a.dot(x) + b
+ #print("X: " + str(x))
+ #print("Y: " + str(y))
+ return space.fromCoords(y[0][0], y[1][0], y[2][0])
+
+def transform(a, b, x):
+ #print(type(x))
+ return a.dot(x[:,None]) + b
+
+def rhoC(space, color1, color2):
+ x1,y1,z1 = space.getCoords(color1)
+ x2,y2,z2 = space.getCoords(color2)
+ return sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
+
+def rho(c1, c2):
+ x1,y1,z1 = c1[0], c1[1], c1[2]
+ x2,y2,z2 = c2[0], c2[1], c2[2]
+ return sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
+
+def get_center(points):
+ zero = np.array([0,0,0])
+ return sum(points, zero) / float(len(points))
+
+def get_center_color(space, colors):
+ zero = np.array([0,0,0])
+ points = [space.getCoords(c) for c in colors]
+ center = sum(points, zero) / float(len(points))
+ c1,c2,c3 = center[0], center[1], center[2]
+ return space.fromCoords((c1,c2,c3))
+
+def get_nearest(x, occupied, points):
+ min_rho = None
+ min_p = None
+ for p in points:
+ if any([(p == o).all() for o in occupied]):
+ continue
+ r = rho(x,p)
+ if min_rho is None or r < min_rho:
+ min_rho = r
+ min_p = p
+ if min_p is not None:
+ occupied.append(min_p)
+ #print("Now occupied : " + str(x))
+ return min_p
+ #print("Was occupied : " + str(x))
+ for p in points:
+ r = rho(x,p)
+ if min_rho is None or r < min_rho:
+ min_rho = r
+ min_p = p
+ return min_p
+
+def get_nearest_color(space, occupied, cx, colors):
+ points = [space.getCoords(c) for c in colors]
+ cy = get_nearest(space.getCoords(cx), occupied, points)
+ return space.fromCoords((cy[0], cy[1], cy[2]))
+
+def exclude(lst, x):
+ return [p for p in lst if not (p == x).all()]
+
+def get_farest(points):
+ #center = get_center(points)
+ #print(str(center))
+ points_ = copy(points)
+ darkest = min(points_, key = lambda p: p[2])
+ points_ = exclude(points_, darkest)
+ lightest = max(points_, key = lambda p: p[2])
+ points_ = exclude(points_, lightest)
+ grayest = min(points_, key = lambda p: p[1])
+ points_ = exclude(points_, grayest)
+ most_saturated = max(points_, key = lambda p: p[1])
+ return [darkest, lightest, grayest, most_saturated]
+ #srt = sorted(points, key = lambda c: -rho(center, c))
+ #return srt[:4]
+
+def get_farest_colors(space, colors):
+ points = [space.getCoords(c) for c in colors]
+ farest = get_farest(points)
+ return [space.fromCoords(c) for c in farest]
+
+def match_colors_(space, colors1, colors2):
+ try:
+ points1 = [color_row(space, c) for c in colors1 if c is not None]
+ points2 = [color_row(space, c) for c in colors2 if c is not None]
+ farest1 = get_farest(points1)
+ farest2 = get_farest(points2)
+ a, b = find_transform(farest1, farest2)
+ #print("A:\n" + str(a))
+ #print("B:\n" + str(b))
+ print("Matching colors:")
+ for p1, p2 in zip(farest1, farest2):
+ print(" {} -> {}".format(p1,p2))
+ transformed = [transform(a, b, x) for x in points1]
+ occupied = []
+ matched = []
+ for x in transformed:
+ y = get_nearest(x, occupied, points2)
+ matched.append(y)
+ return [space.fromCoords(x) for x in matched]
+ except LinAlgError as e:
+ print e
+ return colors1
+
+def match_colors(space, colors1, colors2):
+ points1 = [color_row(space, c) for c in colors1 if c is not None]
+ points2 = [color_row(space, c) for c in colors2 if c is not None]
+ c1 = get_center(points1)
+ c2 = get_center(points2)
+ #print c1, c2
+ print("Centers difference: {}".format(c2-c1))
+ szs1 = np.vstack([abs(c1-p) for p in points1]).max(axis=0)
+ szs2 = np.vstack([abs(c2-p) for p in points2]).max(axis=0)
+ if (szs1 == 0).any():
+ zoom = np.array([1,1,1])
+ else:
+ zoom = szs2 / szs1
+ print("Scale by axes: {}".format(zoom))
+ transformed = [(p-c1)*zoom + c2 for p in points1]
+ occupied = []
+ matched = []
+ deltas = []
+ for x in transformed:
+ y = get_nearest(x, occupied, points2)
+ matched.append(y)
+ deltas.append(abs(y-x))
+ delta = np.vstack(deltas).max(axis=0)
+ print("Maximum deltas from affine transformed to source colors: {}".format(delta))
+ return [space.fromCoords(x) for x in matched]
+
+def find_simple_transform(space, colors1, colors2):
+ points1 = [color_row(space, c) for c in colors1 if c is not None]
+ points2 = [color_row(space, c) for c in colors2 if c is not None]
+ c1 = get_center(points1)
+ c2 = get_center(points2)
+ #transfer = c2 - c1
+ szs1 = max([abs(c1-p) for p in points1])
+ szs2 = max([abs(c2-p) for p in points2])
+ if (szs1 == 0).any():
+ zoom = np.array([1,1,1])
+ else:
+ zoom = szs2 / szs1
+ return (c1, c2, zoom)
+
+if __name__ == "__main__":
+
+ x1 = Color(0,0,0)
+ x2 = Color(0,10,0)
+ x3 = Color(0,0,20)
+ x4 = Color(20,0,10)
+ x5 = Color(10,10,10)
+ x6 = Color(0,20,0)
+ x7 = Color(0,0,40)
+ x8 = Color(40,0,20)
+
+ y1 = Color(0,0,0)
+ y2 = Color(0,10,0)
+ y3 = Color(0,0,20)
+ y4 = Color(20,0,10)
+ y5 = Color(10,10,10)
+ y6 = Color(0,20,0)
+ y7 = Color(0,0,40)
+ y8 = Color(40,0,20)
+
+ res = match_colors(RGB, [x1,x2,x3,x4,x5,x6,x7,x8], [y1,y2,y3,y4,y5,y6,y7,y8])
+ print([c.getRGB() for c in res])
+
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/open_palette.py b/extensions/fablabchemnitz/color_harmony/color_harmony/open_palette.py
new file mode 100644
index 0000000..dd795fd
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/open_palette.py
@@ -0,0 +1,17 @@
+def load_palette(filename, mixer=None, options=None):
+ if mixer is None:
+ mixer = mixers.MixerRGB
+ loader = detect_storage(filename)
+ if loader is None:
+ return None
+ palette = loader().load(mixer, filename, options)
+ return palette
+
+def save_palette(palette, path, formatname=None):
+ if formatname is not None:
+ loader = get_storage_by_name(formatname)
+ else:
+ loader = detect_storage(path, save=True)
+ if loader is None:
+ raise RuntimeError("Unknown file type!")
+ loader(palette).save(path)
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/palette/palette.py b/extensions/fablabchemnitz/color_harmony/color_harmony/palette/palette.py
new file mode 100644
index 0000000..698773c
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/palette/palette.py
@@ -0,0 +1,352 @@
+
+from os.path import join, basename
+
+from color.colors import *
+from color import mixers
+from models.meta import Meta
+
+NONE = 0
+USER_DEFINED = 1
+VERTICALLY_GENERATED = 2
+HORIZONTALLY_GENERATED = 3
+DEFAULT_GROUP_SIZE = 7
+MAX_COLS = 10
+
+class Slot(object):
+ def __init__(self, color=None, name=None, user_defined=False):
+ self._color = color
+ self._mode = NONE
+ self._user_defined = user_defined
+ self._src_slot1 = None
+ self._src_row1 = None
+ self._src_col1 = None
+ self._src_slot2 = None
+ self._src_row2 = None
+ self._src_col2 = None
+ if name:
+ self.name = name
+
+ def get_name(self):
+ if self._color:
+ return self._color.name
+ else:
+ return None
+
+ def set_name(self, name):
+ if self._color:
+ self._color.name = name
+
+ name = property(get_name, set_name)
+
+ def __repr__(self):
+ return "".format(self._mode)
+
+ def getColor(self):
+ if self._color is None:
+ return Color(1,1,1)
+ else:
+ return self._color
+
+ def setColor(self, color):
+ self._color = color
+
+ color = property(getColor, setColor)
+
+ def getMode(self):
+ if self._user_defined:
+ return USER_DEFINED
+ else:
+ return self._mode
+
+ def setMode(self, mode):
+ self._mode = mode
+ if mode == USER_DEFINED:
+ self._user_defined = True
+ self._mode = NONE
+
+ mode = property(getMode, setMode)
+
+ def mark(self, user_defined=None):
+ if user_defined is None:
+ user_defined = not self._user_defined
+ #print("Mark: " + str(user_defined))
+ self._user_defined = user_defined
+
+ def setSources(self, slot1, row1, col1, slot2, row2, col2):
+ self._src_slot1 = slot1
+ self._src_row1 = row1
+ self._src_col1 = col1
+ self._src_slot2 = slot2
+ self._src_row2 = row2
+ self._src_col2 = col2
+ #print("Sources: ({},{}) and ({},{})".format(row1,col1, row2,col2))
+
+ def getSource1(self):
+ return self._src_slot1, self._src_row1, self._src_col1
+
+ def getSource2(self):
+ return self._src_slot2, self._src_row2, self._src_col2
+
+class Palette(object):
+ def __init__(self, mixer, nrows=1, ncols=7):
+ self.mixer = mixer
+ self.nrows = nrows
+ self.ncols = ncols
+ self.slots = [[Slot() for i in range(ncols)] for j in range(nrows)]
+ self.need_recalc_colors = True
+ self.meta = Meta()
+
+ def get_name(self):
+ return self.meta.get("Name", "Untited")
+
+ def set_name(self, name):
+ self.meta["Name"] = name
+
+ name = property(get_name, set_name)
+
+ def mark_color(self, row, column, mark=None):
+ print("Marking color at ({}, {})".format(row,column))
+ slot = self.slots[row][column]
+ slot.mark(mark)
+ self.need_recalc_colors = True
+ self.recalc()
+
+ def setMixer(self, mixer):
+ self.mixer = mixer
+ self.recalc()
+
+ def del_column(self, col):
+ if self.ncols < 2:
+ return
+ new = []
+ for row in self.slots:
+ new_row = []
+ for c, slot in enumerate(row):
+ if c != col:
+ new_row.append(slot)
+ new.append(new_row)
+ self.slots = new
+ self.ncols -= 1
+ self.recalc()
+
+ def add_column(self, col):
+ new = []
+ for row in self.slots:
+ new_row = []
+ for c, slot in enumerate(row):
+ if c == col:
+ new_row.append(Slot())
+ new_row.append(slot)
+ new.append(new_row)
+ self.slots = new
+ self.ncols += 1
+ self.recalc()
+
+ def del_row(self, row):
+ if self.nrows < 2:
+ return
+ new = []
+ for r,row_ in enumerate(self.slots):
+ if r != row:
+ new.append(row_)
+ self.slots = new
+ self.nrows -= 1
+ self.recalc()
+
+ def add_row(self, row):
+ new = []
+ for r,row_ in enumerate(self.slots):
+ if r == row:
+ new_row = [Slot() for k in range(self.ncols)]
+ new.append(new_row)
+ new.append(row_)
+ self.slots = new
+ self.nrows += 1
+ self.recalc()
+
+ def paint(self, row, column, color):
+ self.slots[row][column].color = color
+ self.slots[row][column].mark(True)
+ #self.recalc()
+
+ def erase(self, row, column):
+ self.paint(row, column, Color(0,0,0))
+
+ def getUserDefinedSlots(self):
+ """Returns list of tuples: (row, column, slot)."""
+ result = []
+ for i,row in enumerate(self.slots):
+ for j,slot in enumerate(row):
+ if slot.mode == USER_DEFINED:
+ result.append((i,j,slot))
+ return result
+
+ def getColor(self, row, column):
+ if self.need_recalc_colors:
+ self.recalc()
+ try:
+ return self.slots[row][column].color
+ except IndexError:
+ #print("Cannot get color ({},{}): size is ({},{})".format(row,column, self.nrows, self.ncols))
+ return Color(255,255,255)
+
+ def getColors(self):
+ if self.need_recalc_colors:
+ self.recalc()
+ return [[slot.color for slot in row] for row in self.slots]
+
+ def setSlots(self, all_slots):
+ m = len(all_slots) % self.ncols
+ if m != 0:
+ for i in range(self.ncols - m):
+ all_slots.append(Slot(Color(255,255,255)))
+ self.slots = []
+ row = []
+ for i, slot in enumerate(all_slots):
+ if i % self.ncols == 0:
+ if len(row) != 0:
+ self.slots.append(row)
+ row = []
+ row.append(slot)
+ self.slots.append(row)
+ self.nrows = len(self.slots)
+ self.need_recalc_colors = True
+ self.recalc()
+# print(self.slots)
+
+ def user_chosen_slot_down(self, i,j):
+ """Returns tuple:
+ * Is slot found
+ * Row of found slot
+ * Column of found slot"""
+
+ #print("Searching down ({},{})".format(i,j))
+ for i1 in range(i, self.nrows):
+ try:
+ slot = self.slots[i1][j]
+ #print("Down: check ({},{}): {}".format(i1,j,slot))
+ if slot.mode == USER_DEFINED:
+ #print("Found down ({},{}): ({},{})".format(i,j, i1,j))
+ return True,i1,j
+ except IndexError:
+ print("Cannot get slot at ({}, {})".format(i,j))
+ return False, self.nrows-1,j
+ return False, self.nrows-1,j
+
+ def user_chosen_slot_up(self, i,j):
+ """Returns tuple:
+ * Is slot found
+ * Row of found slot
+ * Column of found slot"""
+
+ for i1 in range(i-1, -1, -1):
+ if self.slots[i1][j].mode == USER_DEFINED:
+ #print("Found up ({},{}): ({},{})".format(i,j, i1,j))
+ return True,i1,j
+ return False, 0, j
+
+ def fixed_slot_right(self, i,j):
+ """Returns tuple:
+ * Mode of found slot
+ * Row of found slot
+ * Column of found slot"""
+
+ for j1 in range(j, self.ncols):
+ if self.slots[i][j1].mode in [USER_DEFINED, VERTICALLY_GENERATED]:
+ return self.slots[i][j1].mode, i,j1
+ return NONE, i,self.ncols-1
+
+ def fixed_slot_left(self, i,j):
+ """Returns tuple:
+ * Mode of found slot
+ * Row of found slot
+ * Column of found slot"""
+
+ for j1 in range(j-1, -1, -1):
+ if self.slots[i][j1].mode in [USER_DEFINED, VERTICALLY_GENERATED]:
+ return self.slots[i][j1].mode, i,j1
+ return NONE, i,0
+
+ def recalc(self):
+ self._calc_modes()
+ self._calc_modes()
+ self._calc_modes()
+ self._calc_colors()
+ self.need_recalc_colors = False
+
+ def _calc_modes(self):
+ for i,row in enumerate(self.slots):
+ for j,slot in enumerate(row):
+ if slot.mode == USER_DEFINED:
+ continue
+ # Should slot be vertically generated?
+ v1,iv1,jv1 = self.user_chosen_slot_down(i,j)
+ v2,iv2,jv2 = self.user_chosen_slot_up(i,j)
+ h1,ih1,jh1 = self.fixed_slot_left(i,j)
+ h2,ih2,jh2 = self.fixed_slot_right(i,j)
+ if v1 and v2: # if there are user chosen slots above and below current
+ slot.mode = VERTICALLY_GENERATED
+ s1 = self.slots[iv1][jv1]
+ s2 = self.slots[iv2][jv2]
+ slot.setSources(s1, iv1, jv1, s2, iv2, jv2)
+ elif ((v1 and j-jv1 > 1) or (v2 and jv2-j > 1)) and ((h1!=USER_DEFINED) or (h2!=USER_DEFINED)):
+ slot.mode = VERTICALLY_GENERATED
+ s1 = self.slots[iv1][jv1]
+ s2 = self.slots[iv2][jv2]
+ slot.setSources(s1, iv1, jv1, s2, iv2, jv2)
+ elif h1 and h2:
+ # if there are fixed slots at left and at right of current
+ slot.mode = HORIZONTALLY_GENERATED
+ s1 = self.slots[ih1][jh1]
+ s2 = self.slots[ih2][jh2]
+ slot.setSources(s1, ih1, jh1, s2, ih2, jh2)
+ elif (h1 or h2) and not (v1 or v2):
+ slot.mode = HORIZONTALLY_GENERATED
+ s1 = self.slots[ih1][jh1]
+ s2 = self.slots[ih2][jh2]
+ slot.setSources(s1, ih1, jh1, s2, ih2, jh2)
+ else:
+ slot.mode = HORIZONTALLY_GENERATED
+ s1 = self.slots[ih1][jh1]
+ s2 = self.slots[ih2][jh2]
+ slot.setSources(s1, ih1, jh1, s2, ih2, jh2)
+
+ def color_transition(self, from_color, to_color, steps, idx):
+ if self.mixer is None:
+ return Color(1,1,1)
+ q = float(idx+1) / float(steps+1)
+ return self.mixer.mix(from_color, to_color, q)
+
+ def _calc_colors(self):
+ for i,row in enumerate(self.slots):
+ for j,slot in enumerate(row):
+ if slot.mode == VERTICALLY_GENERATED:
+ slot_down, iv1, jv1 = slot.getSource1()
+ slot_up, iv2, jv2 = slot.getSource2()
+ clr_down = slot_down.color
+ clr_up = slot_up.color
+ length = iv1-iv2 - 1
+ idx = i-iv2 - 1
+ try:
+ #print("Mixing ({},{}) with ({},{}) to get ({},{})".format(iv1,jv1, iv2,jv2, i,j))
+ clr = self.color_transition(clr_up,clr_down,length, idx)
+ except IndexError:
+ clr = Color(1,1,1)
+ slot.color = clr
+
+ for i,row in enumerate(self.slots):
+ for j,slot in enumerate(row):
+ if slot.mode == HORIZONTALLY_GENERATED:
+ slot_left, ih1, jh1 = slot.getSource1()
+ slot_right, ih2, jh2 = slot.getSource2()
+ clr_left = slot_left.color
+ clr_right = slot_right.color
+ length = jh2-jh1 - 1
+ idx = j-jh1 - 1
+ try:
+ #print("Mixing ({},{}) with ({},{}) to get ({},{})".format(ih1,jh1, ih2,jh2, i,j))
+ clr = self.color_transition(clr_left,clr_right,length, idx)
+ except IndexError:
+ clr = Color(1,1,1)
+ slot.color = clr
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/shades.py b/extensions/fablabchemnitz/color_harmony/color_harmony/shades.py
new file mode 100644
index 0000000..7f2a9f1
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/shades.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+from color_harmony.colorplus import ColorPlus
+from color_harmony.utils import clip, seq, variate
+
+# Shading functions
+
+# 4 cooler colors
+def cooler(color, parameter):
+ h,s,v = color.to_hsv()
+ if h < 1.0/6.0:
+ sign = -1.0
+ elif h > 2.0/3.0:
+ sign = -1.0
+ else:
+ sign = 1.0
+ step = 0.1 * parameter
+ result = []
+ for i in range(4):
+ h += sign * step
+ if h > 1.0:
+ h -= 1.0
+ elif h < 0.0:
+ h += 1.0
+ result.append(ColorPlus.from_hsv(h, s, v))
+ return result
+
+# 4 warmer colors
+def warmer(color, parameter):
+ h,s,v = color.to_hsv()
+ if h < 1.0/6.0:
+ sign = +1.0
+ elif h > 2.0/3.0:
+ sign = +1.0
+ else:
+ sign = -1.0
+ step = 0.1 * parameter
+ result = []
+ for i in range(4):
+ h += sign * step
+ if h > 1.0:
+ h -= 1.0
+ elif h < 0.0:
+ h += 1.0
+ result.append(ColorPlus.from_hsv(h, s, v))
+ return result
+
+# returns 2 less saturated and 2 more saturated colors
+def saturation(color, parameter):
+ h, s, v = color.to_hsv()
+ ss = [clip(x) for x in variate(s, 0.6*parameter, 1.2*parameter)]
+ # we don't want another copy of the original color
+ del ss[2]
+ return [ColorPlus.from_hsv(h, s, v) for s in ss]
+
+# 2 colors with higher value, and 2 with lower
+def value(color, parameter):
+ h, s, v = color.to_hsv()
+ vs = [clip(x) for x in variate(v, 0.4*parameter, 0.8*parameter)]
+ del vs[2]
+ return [ColorPlus.from_hsv(h, s, v) for v in vs]
+
+# 2 colors with higher chroma, and 2 with lower
+def chroma(color, parameter):
+ h, c, y = color.to_hcy()
+ cs = [clip(x) for x in variate(c, 0.4*parameter, 0.8*parameter)]
+ del cs[2]
+ return [ColorPlus.from_hcy(h, c, y) for c in cs]
+
+# 2 colors with higher luma, and 2 with lower
+def luma(color, parameter):
+ h, c, y = color.to_hcy()
+ ys = [clip(x) for x in variate(y, 0.3*parameter, 0.6*parameter)]
+ del ys[2]
+ return [ColorPlus.from_hcy(h, c, y) for y in ys]
+
+# 2 colors with hue rotated to the left, and 2 rotated to the right
+def hue(color, parameter):
+ h, c, y = color.to_hcy()
+ hs = [clip(x) for x in variate(h, 0.15*parameter, 0.3*parameter)]
+ del hs[2]
+ return [ColorPlus.from_hcy(h, c, y) for h in hs]
+
+def hue_luma(color, parameter):
+ h, c, y = color.to_hcy()
+ hs = [clip(x) for x in variate(h, 0.15*parameter, 0.3*parameter)]
+ ys = [clip(x) for x in variate(y, 0.3*parameter, 0.6*parameter)]
+ del ys[2]
+ del hs[2]
+ return [ColorPlus.from_hcy(h, c, y) for h,y in zip(hs, ys)]
+
+def luma_plus_chroma(color, parameter):
+ h, c, y = color.to_hcy()
+ cs = [clip(x) for x in variate(c, 0.4*parameter, 0.8*parameter)]
+ ys = [clip(x) for x in variate(y, 0.3*parameter, 0.6*parameter)]
+ del cs[2]
+ del ys[2]
+ return [ColorPlus.from_hcy(h, c, y) for c,y in zip(cs, ys)]
+
+def luma_minus_chroma(color, parameter):
+ h, c, y = color.to_hcy()
+ cs = [clip(x) for x in variate(c, 0.4*parameter, 0.8*parameter)]
+ ys = list(reversed([clip(x) for x in variate(y, 0.3*parameter, 0.6*parameter)]))
+ del cs[2]
+ del ys[2]
+ return [ColorPlus.from_hcy(h, c, y) for c,y in zip(cs, ys)]
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/__init__.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/__init__.py
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/__init__.py
@@ -0,0 +1 @@
+# empty
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/cluster.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/cluster.py
new file mode 100644
index 0000000..8ff0c81
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/cluster.py
@@ -0,0 +1,180 @@
+
+from math import sqrt, isnan
+import numpy as np
+
+try:
+ from sklearn.cluster import MeanShift, estimate_bandwidth
+ from sklearn.utils import shuffle
+
+ cluster_analysis_available = True
+ print("Cluster analysis is available")
+except ImportError:
+ cluster_analysis_available = False
+ print("Cluster analysis is not available")
+
+try:
+ import Image
+ pil_available = True
+ print("PIL is available")
+except ImportError:
+ print("PIL is not available")
+ try:
+ from PIL import Image
+ print("Pillow is available")
+ pil_available = True
+ except ImportError:
+ print("Neither PIL or Pillow are available")
+ pil_available = False
+
+from color.colors import *
+
+if pil_available:
+
+ def imread(filename):
+ img = Image.open(filename)
+
+class Box(object):
+ def __init__(self, arr):
+ self._array = arr
+
+ def population(self):
+ return self._array.size
+
+ def axis_size(self, idx):
+ slice_ = self._array[:, idx]
+ M = slice_.max()
+ m = slice_.min()
+ return M - m
+
+ def biggest_axis(self):
+ sizes = [self.axis_size(i) for i in range(3)]
+ return max(range(3), key = lambda i: sizes[i])
+
+ def mean(self):
+ return self._array.mean(axis=0)
+
+ def mean_color(self):
+ size = self._array.size
+ if not size:
+ return None
+ xs = self._array.mean(axis=0)
+ x,y,z = xs[0], xs[1], xs[2]
+ if isnan(x) or isnan(y) or isnan(z):
+ return None
+ return Color(int(x), int(y), int(z))
+
+ def div_pos(self, idx):
+ slice_ = self._array[:, idx]
+ M = slice_.max()
+ m = slice_.min()
+ return (m+M)/2.0
+
+ def divide(self):
+ axis = self.biggest_axis()
+ q = self.div_pos(axis)
+ idxs = self._array[:, axis] > q
+ smaller = self._array[~idxs]
+ bigger = self._array[idxs]
+ self._array = smaller
+ return Box(bigger)
+
+if pil_available and cluster_analysis_available:
+
+ # Use Means Shift algorithm for cluster analysis
+
+ def cluster_analyze(filename, N=1000):
+ image = imread(filename)
+ w,h,d = tuple(image.shape)
+ image_array = np.array( np.reshape(image, (w * h, d)), dtype=np.float64 )
+ #if image.dtype == 'uint8':
+ # image_array = image_array / 255.0
+ image_array_sample = shuffle(image_array, random_state=0)[:N]
+ bandwidth = estimate_bandwidth(image_array_sample, quantile=0.01, n_samples=500)
+ ms = MeanShift(bandwidth=bandwidth, bin_seeding=True)
+ ms.fit(image_array_sample)
+ cluster_centers = ms.cluster_centers_
+ n_clusters = len(cluster_centers)
+ colors = []
+ print("Number of clusters: {}".format(n_clusters))
+ for x in cluster_centers:
+ #print x
+ clr = Color()
+ clr.setRGB1((x[0], x[1], x[2]))
+ colors.append(clr)
+ return colors
+
+
+if pil_available:
+
+# Use very fast algorithm for image analysis, translated from Krita's kis_common_colors_recalculation_runner.cpp
+# Do not know exactly why does this algorithm work, but it does.
+# Initial (C) Adam Celarek
+
+ def bin_divide_colors(filename, N=1<<16, n_clusters=49):
+ img = Image.open(filename)
+ if img.mode == 'P':
+ img = img.convert('RGB')
+ w,h = img.size
+ n_pixels = w*h
+ if n_pixels > N:
+ ratio = sqrt( float(N) / float(n_pixels) )
+ w,h = int(w*ratio), int(h*ratio)
+ img = img.resize((w,h))
+
+ image = np.array(img)
+ w,h,d = tuple(image.shape)
+ colors = np.array( np.reshape(image, (w * h, d)), dtype=np.float64 )
+ #if image.dtype == 'uint8':
+ # colors = colors / 255.0
+ colors = colors[:,0:3]
+
+ boxes = [Box(colors)]
+
+ while (len(boxes) < n_clusters * 3/5) and (len(colors) > n_clusters * 3/5):
+ biggest_box = None
+ biggest_box_population = None
+
+ for box in boxes:
+ population = box.population()
+ if population <= 3:
+ continue
+ if biggest_box_population is None or (population > biggest_box_population and box.axis_size(box.biggest_axis()) >= 3):
+ biggest_box = box
+ biggest_box_population = population
+
+ if biggest_box is None or biggest_box.population() <= 3:
+ break
+
+ new_box = biggest_box.divide()
+ boxes.append(new_box)
+
+ while (len(boxes) < n_clusters) and (len(colors) > n_clusters):
+ biggest_box = None
+ biggest_box_axis_size = None
+
+ for box in boxes:
+ if box.population() <= 3:
+ continue
+ size = box.axis_size(box.biggest_axis())
+ if biggest_box_axis_size is None or (size > biggest_box_axis_size and size >= 3):
+ biggest_box = box
+ biggest_box_axis_size = size
+
+ if biggest_box is None or biggest_box.population() <= 3:
+ break
+
+ new_box = biggest_box.divide()
+ boxes.append(new_box)
+
+ result = [box.mean_color() for box in boxes if box.mean_color() is not None]
+ return result
+
+image_loading_supported = pil_available or cluster_analysis_available
+
+if pil_available:
+ get_common_colors = bin_divide_colors
+ use_sklearn = False
+elif cluster_analysis_available :
+ get_common_colors = cluster_analyze
+ use_sklearn = True
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/css.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/css.py
new file mode 100644
index 0000000..9936f45
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/css.py
@@ -0,0 +1,83 @@
+
+from os.path import join, basename
+from math import sqrt, floor
+
+from color.colors import *
+from color import mixers
+from palette.storage.storage import *
+
+try:
+ from tinycss import make_parser, color3
+ css_support = True
+except ImportError:
+ print("TinyCSS is not available")
+ css_support = False
+
+class CSS(Storage):
+ name = 'css'
+ title = _("CSS cascading stylesheet")
+ filters = ["*.css"]
+ can_load = css_support
+ can_save = True
+
+ @staticmethod
+ def check(filename):
+ return True
+
+ def save(self, file_w):
+ pf = open(file_w, 'w')
+
+ for i, row in enumerate(self.palette.slots):
+ for j, slot in enumerate(row):
+ user = "-user" if slot.mode == USER_DEFINED else ""
+ hex = slot.color.hex()
+ s = ".color-{}-{}{} {{ color: {} }};\n".format(i,j, user, hex)
+ pf.write(s)
+
+ pf.close()
+
+ def load(self, mixer, file_r, options=None):
+ self.palette = Palette(mixer)
+ self.palette.ncols = None
+
+ all_slots = []
+ colors = []
+
+ def add_color(clr):
+ for c in colors:
+ if c.getRGB() == clr.getRGB():
+ return None
+ colors.append(clr)
+ return clr
+
+ parser = make_parser('page3')
+ css = parser.parse_stylesheet_file(file_r)
+ for ruleset in css.rules:
+ #print ruleset
+ if ruleset.at_keyword:
+ continue
+ for declaration in ruleset.declarations:
+ #print declaration
+ for token in declaration.value:
+ #print token
+ css_color = color3.parse_color(token)
+ if not isinstance(css_color, color3.RGBA):
+ continue
+ r,g,b = css_color.red, css_color.green, css_color.blue
+ color = Color()
+ color.setRGB1((clip(r), clip(g), clip(b)))
+ color = add_color(color)
+ if not color:
+ continue
+ slot = Slot(color, user_defined=True)
+ all_slots.append(slot)
+ n_colors = len(all_slots)
+ if n_colors > MAX_COLS:
+ self.palette.ncols = max( int( floor(sqrt(n_colors)) ), 1)
+ else:
+ self.palette.ncols = n_colors
+ self.palette.setSlots(all_slots)
+ self.palette.meta["SourceFormat"] = "CSS"
+ print("Loaded palette: {}x{}".format( self.palette.nrows, self.palette.ncols ))
+ return self.palette
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/gimp.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/gimp.py
new file mode 100644
index 0000000..f82b330
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/gimp.py
@@ -0,0 +1,188 @@
+
+from os.path import join, basename
+from PyQt4 import QtGui
+import re
+
+from color.colors import *
+from color import mixers
+from palette.storage.storage import *
+
+marker = '# Colors not marked with #USER are auto-generated'
+
+metare = re.compile("^# (\\w+): (.+)")
+
+def save_gpl(name, ncols, clrs, file_w):
+ if type(file_w) in [str,unicode]:
+ pf = open(file_w, 'w')
+ do_close = True
+ elif hasattr(file_w,'write'):
+ pf = file_w
+ do_close = False
+ else:
+ raise ValueError("Invalid argument type in save_gpl: {}".format(type(file_w)))
+ pf.write('GIMP Palette\n')
+ pf.write(u"Name: {}\n".format(name).encode('utf-8'))
+ if ncols is not None:
+ pf.write('Columns: %s\n' % ncols)
+ for color in clrs:
+ r, g, b = color.getRGB()
+ n = 'Untitled'
+ s = '%d %d %d %s\n' % (r, g, b, n)
+ pf.write(s)
+ if do_close:
+ pf.close()
+
+class GimpPalette(Storage):
+
+ name = 'gpl'
+ title = _("Gimp palette")
+ filters = ["*.gpl"]
+ can_save = True
+ can_load = True
+
+ @staticmethod
+ def get_options_widget(dialog, filename):
+
+ def on_columns_changed(n):
+ dialog.options = n
+ dialog.on_current_changed(filename)
+
+ ncols = None
+ pf = open(filename,'r')
+ l = pf.readline().strip()
+ if l != 'GIMP Palette':
+ pf.close()
+ return None
+ for line in pf:
+ line = line.strip()
+ lst = line.split()
+ if lst[0]=='Columns:':
+ ncols = int( lst[1] )
+ break
+ pf.close()
+
+ widget = QtGui.QWidget()
+ box = QtGui.QHBoxLayout()
+ label = QtGui.QLabel(_("Columns: "))
+ spinbox = QtGui.QSpinBox()
+ spinbox.setMinimum(2)
+ spinbox.setMaximum(100)
+ if ncols is None:
+ ncols = MAX_COLS
+ spinbox.setValue(ncols)
+ box.addWidget(label)
+ box.addWidget(spinbox)
+ spinbox.valueChanged.connect(on_columns_changed)
+ widget.setLayout(box)
+ return widget
+
+ def save(self, file_w=None):
+ if type(file_w) in [str,unicode]:
+ pf = open(file_w, 'w')
+ do_close = True
+ elif hasattr(file_w,'write'):
+ pf = file_w
+ do_close = False
+ else:
+ raise ValueError("Invalid argument type in GimpPalette.save: {}".format(type(file_w)))
+ pf.write('GIMP Palette\n')
+ if not hasattr(self.palette, 'name'):
+ if type(file_w) in [str, unicode]:
+ self.palette.name = basename(file_w)
+ else:
+ self.palette.name='Colors'
+ pf.write(u"Name: {}\n".format(self.palette.name).encode('utf-8'))
+ if hasattr(self.palette, 'ncols') and self.palette.ncols:
+ pf.write('Columns: %s\n' % self.palette.ncols)
+ pf.write(marker+'\n')
+ for key,value in self.palette.meta.items():
+ if key != "Name":
+ pf.write(u"# {}: {}\n".format(key, value).encode('utf-8'))
+ pf.write('#\n')
+ for row in self.palette.slots:
+ for slot in row:
+ if slot.mode == USER_DEFINED:
+ n = slot.name + ' #USER'
+ else:
+ n = slot.name
+ r, g, b = slot.color.getRGB()
+ s = '%d %d %d %s\n' % (r, g, b, n)
+ pf.write(s)
+ for key,value in slot.color.meta.items():
+ if key != "Name":
+ pf.write(u"# {}: {}\n".format(key, value).encode('utf-8'))
+ if do_close:
+ pf.close()
+
+ def load(self, mixer, file_r, force_ncols=None):
+ self.palette = Palette(mixer)
+ self.palette.ncols = None
+ if not file_r:
+ palette.filename = None
+ palette.name = 'Gimp'
+ return
+ elif hasattr(file_r,'read'):
+ pf = file_r
+ self.palette.filename = None
+ do_close = False
+ elif type(file_r) in [str,unicode]:
+ pf = open(file_r)
+ self.palette.filename = file_r
+ do_close = True
+ l = pf.readline().strip()
+ if l != 'GIMP Palette':
+ raise SyntaxError, "Invalid palette file!"
+ self.palette.name = " ".join(pf.readline().strip().split()[1:])
+ all_user = True
+ n_colors = 0
+ all_slots = []
+ reading_header = True
+ for line in pf:
+ line = line.strip()
+ if line==marker:
+ all_user = False
+ meta_match = metare.match(line)
+ if meta_match is not None:
+ key = meta_match.group(1)
+ value = meta_match.group(2)
+ if reading_header:
+ self.palette.meta[key] = value
+ else:
+ clr.meta[key] = value
+ continue
+ if line.startswith('#'):
+ continue
+ lst = line.split()
+ if lst[0]=='Columns:':
+ self.palette.ncols = int( lst[1] )
+ if len(lst) < 3:
+ continue
+ rs,gs,bs = lst[:3]
+ clr = Color(float(rs), float(gs), float(bs))
+ reading_header = False
+ #print(str(clr))
+ slot = Slot(clr)
+ n_colors += 1
+ if all_user or lst[-1]=='#USER':
+ slot.mode = USER_DEFINED
+ name = ' '.join(lst[3:-1])
+ else:
+ name = ' '.join(lst[3:])
+ slot.name = name
+ all_slots.append(slot)
+ if do_close:
+ pf.close()
+ if n_colors < DEFAULT_GROUP_SIZE:
+ self.palette.ncols = n_colors
+ if not self.palette.ncols:
+ if n_colors > MAX_COLS:
+ self.palette.ncols = MAX_COLS
+ else:
+ self.palette.ncols = n_colors
+ if force_ncols is not None:
+ self.palette.ncols = force_ncols
+ self.palette.setSlots(all_slots)
+ self.palette.meta["SourceFormat"] = "Gimp gpl" if all_user else "palette_editor gpl"
+ print("Loaded palette: {}x{}".format( self.palette.nrows, self.palette.ncols ))
+ return self.palette
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/image.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/image.py
new file mode 100644
index 0000000..5178e93
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/image.py
@@ -0,0 +1,201 @@
+
+from os.path import join, basename, splitext
+from math import sqrt, floor
+from PyQt4 import QtGui
+
+from color.colors import *
+from color import spaces
+from color import mixers
+from palette.image import PaletteImage
+from palette.palette import *
+from palette.storage.storage import *
+from palette.storage.cluster import *
+from palette.storage.table import parse_color_table
+from matching.transform import rho, get_center
+
+LOAD_MORE = 1
+LOAD_LESS_COMMON = 2
+LOAD_LESS_FAREST = 3
+LOAD_TABLE = 4
+
+print("Ok")
+
+class DialogOptions(object):
+ def __init__(self, method):
+ self.method = method
+ self.border_x = 10
+ self.border_y = 10
+ self.gap_x = 10
+ self.gap_y = 10
+ self.size_x = 5
+ self.size_y = 5
+
+class Image(Storage):
+ name = 'image'
+ title = _("Raster image")
+ filters = ["*.jpg", "*.png", "*.gif"]
+ can_load = image_loading_supported
+ can_save = True
+
+ @staticmethod
+ def check(filename):
+ return True
+
+ @staticmethod
+ def get_options_widget(dialog, filename):
+ if use_sklearn:
+ return None
+
+ def dependencies():
+ if dialog.options is None or dialog.options.method != LOAD_TABLE:
+ table_w.setVisible(False)
+ else:
+ table_w.setVisible(True)
+
+ def on_method_changed(checked):
+ method = None
+ if dialog._more_button.isChecked():
+ method = LOAD_MORE
+ elif dialog._less_button.isChecked():
+ method = LOAD_LESS_COMMON
+ elif dialog._less_farest.isChecked():
+ method = LOAD_LESS_FAREST
+ elif dialog._table.isChecked():
+ method = LOAD_TABLE
+ dialog.options = DialogOptions(method)
+ if method == LOAD_TABLE:
+ dialog.options.border_x = dialog._border_x.value()
+ dialog.options.border_y = dialog._border_y.value()
+ dialog.options.gap_x = dialog._gap_x.value()
+ dialog.options.gap_y = dialog._gap_y.value()
+ dialog.options.size_x = dialog._size_x.value()
+ dialog.options.size_y = dialog._size_y.value()
+ dependencies()
+ dialog.on_current_changed(filename)
+
+ group_box = QtGui.QGroupBox(_("Loading method"))
+ dialog._more_button = more = QtGui.QRadioButton(_("Use 49 most used colors"))
+ dialog._less_button = less = QtGui.QRadioButton(_("Use 9 most used colors and mix them"))
+ dialog._less_farest = less_farest = QtGui.QRadioButton(_("Use 9 most different colors and mix them"))
+ dialog._table = table = QtGui.QRadioButton(_("Load table of colors"))
+
+ table_w = QtGui.QWidget(dialog)
+ table_form = QtGui.QFormLayout(table_w)
+ dialog._border_x = QtGui.QSpinBox(table_w)
+ dialog._border_x.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Border from right/left side, px"), dialog._border_x)
+ dialog._border_y = QtGui.QSpinBox(table_w)
+ dialog._border_y.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Border from top/bottom side, px"), dialog._border_y)
+ dialog._gap_x = QtGui.QSpinBox(table_w)
+ dialog._gap_x.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Width of gap between cells, px"), dialog._gap_x)
+ dialog._gap_y = QtGui.QSpinBox(table_w)
+ dialog._gap_y.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Height of gap between cells, px"), dialog._gap_y)
+ dialog._size_x = QtGui.QSpinBox(table_w)
+ dialog._size_x.setValue(5)
+ dialog._size_x.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Number of columns in the table"), dialog._size_x)
+ dialog._size_y = QtGui.QSpinBox(table_w)
+ dialog._size_y.setValue(5)
+ dialog._size_y.valueChanged.connect(on_method_changed)
+ table_form.addRow(_("Number of rows in the table"), dialog._size_y)
+ table_w.setLayout(table_form)
+
+ dependencies()
+
+ more.toggled.connect(on_method_changed)
+ less.toggled.connect(on_method_changed)
+ less_farest.toggled.connect(on_method_changed)
+ table.toggled.connect(on_method_changed)
+
+ if dialog.options is None or dialog.options.method == LOAD_MORE:
+ more.setChecked(True)
+ elif dialog.options.method == LOAD_LESS_COMMON:
+ less.setChecked(True)
+ elif dialog.options.method == LOAD_TABLE:
+ table.setChecked(True)
+ else:
+ less_farest.setChecked(True)
+
+ vbox = QtGui.QVBoxLayout()
+ vbox.addWidget(more)
+ vbox.addWidget(less)
+ vbox.addWidget(less_farest)
+ vbox.addWidget(table)
+ vbox.addWidget(table_w)
+ group_box.setLayout(vbox)
+ return group_box
+
+ def save(self, file_w):
+ w,h = self.palette.ncols * 48, self.palette.nrows * 48
+ image = PaletteImage( self.palette ).get(w,h)
+ print("Writing image: " + file_w)
+ res = image.save(file_w)
+ if not res:
+ image.save(file_w, format='PNG')
+
+ def load(self, mixer, file_r, options=None):
+ def _cmp(clr1,clr2):
+ h1,s1,v1 = clr1.getHSV()
+ h2,s2,v2 = clr2.getHSV()
+ x = cmp(h1,h2)
+ if x != 0:
+ return x
+ x = cmp(v1,v2)
+ if x != 0:
+ return x
+ return cmp(s1,s2)
+
+ def get_farest(space, colors, n=9):
+ points = [space.getCoords(c) for c in colors]
+ center = get_center(points)
+ srt = sorted(points, key = lambda c: -rho(center, c))
+ farest = srt[:n]
+ return [space.fromCoords(c) for c in farest]
+
+ if use_sklearn or options is None or options.method is None or options.method == LOAD_MORE:
+ colors = get_common_colors(file_r)
+ colors.sort(cmp=_cmp)
+ self.palette = create_palette(colors, mixer)
+ return self.palette
+
+ elif options.method == LOAD_TABLE:
+ self.palette = palette = Palette(mixer, nrows=options.size_y, ncols=options.size_x)
+ colors = parse_color_table(file_r, options)
+
+ for i in range(0, options.size_y):
+ for j in range(0, options.size_x):
+ palette.paint(i, j, colors[i][j])
+
+ return palette
+
+ else:
+ if options.method == LOAD_LESS_FAREST:
+ colors = get_common_colors(file_r)
+ colors = get_farest(spaces.RGB, colors)
+ else:
+ colors = get_common_colors(file_r, n_clusters=9)
+
+ self.palette = palette = Palette(mixer, nrows=7, ncols=7)
+
+ palette.paint(0, 0, colors[0])
+ palette.paint(0, 3, colors[1])
+ palette.paint(0, 6, colors[2])
+ palette.paint(3, 0, colors[3])
+ palette.paint(3, 3, colors[4])
+ palette.paint(3, 6, colors[5])
+ palette.paint(6, 0, colors[6])
+ palette.paint(6, 3, colors[7])
+ palette.paint(6, 6, colors[8])
+
+ palette.need_recalc_colors = True
+ palette.recalc()
+
+ name,ext = splitext(basename(file_r))
+ self.palette.meta["SourceFormat"] = ext
+
+ return palette
+
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/kpl.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/kpl.py
new file mode 100644
index 0000000..275b2be
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/kpl.py
@@ -0,0 +1,178 @@
+
+from os.path import join, basename
+from PyQt4 import QtGui
+from lxml import etree as ET
+from zipfile import ZipFile, ZIP_DEFLATED
+
+from color.colors import *
+from color import mixers
+from palette.storage.storage import *
+
+MIMETYPE = "application/x-krita-palette"
+DEFAULT_GROUP_NAME = "Default (root)"
+
+class KplPalette(Storage):
+ name = 'kpl'
+ title = _("Krita 4.0+ palette format")
+ filters = ["*.kpl"]
+ can_load = True
+ can_save = True
+
+ @staticmethod
+ def check(filename):
+ try:
+ with ZipFile(filename, 'r') as zf:
+ mimetype = zf.read("mimetype")
+ return (mimetype == MIMETYPE)
+ except Exception as e:
+ print(e)
+ return False
+
+ @staticmethod
+ def get_group_names(filename):
+ result = [DEFAULT_GROUP_NAME]
+
+ with ZipFile(filename, 'r') as zf:
+ colorset_str = zf.read("colorset.xml")
+ colorset = ET.fromstring(colorset_str)
+
+ for xmlgrp in colorset.xpath("//Group"):
+ name = xmlgrp.attrib['name']
+ if name is not None:
+ result.append(name)
+
+ return result
+
+ @staticmethod
+ def get_options_widget(dialog, filename):
+
+ dialog.options = DEFAULT_GROUP_NAME
+
+ def on_group_changed(selector):
+ def handler():
+ dialog.options = selector.currentText()
+ dialog.on_current_changed(filename)
+ return handler
+
+ widget = QtGui.QWidget()
+ box = QtGui.QHBoxLayout()
+ label = QtGui.QLabel(_("Group: "))
+ selector = QtGui.QComboBox()
+
+ for group_name in KplPalette.get_group_names(filename):
+ selector.addItem(group_name)
+
+ selector.currentIndexChanged.connect(on_group_changed(selector))
+ selector.setCurrentIndex(0)
+
+ box.addWidget(label)
+ box.addWidget(selector)
+ widget.setLayout(box)
+ return widget
+
+ def load(self, mixer, file_r, group_name):
+
+ if group_name is None:
+ group_name = DEFAULT_GROUP_NAME
+
+ def find_group(xml):
+ if group_name == DEFAULT_GROUP_NAME:
+ return xml
+ for xmlgrp in xml.xpath("//Group"):
+ if xmlgrp.attrib['name'] == group_name:
+ return xmlgrp
+ return None
+
+ self.palette = Palette(mixer)
+
+ with ZipFile(file_r, 'r') as zf:
+ mimetype = zf.read("mimetype")
+ if mimetype != MIMETYPE:
+ raise Exception("This is not a valid Krita palette file")
+
+ colorset_str = zf.read("colorset.xml")
+ colorset = ET.fromstring(colorset_str)
+ self.palette.ncols = int( colorset.attrib['columns'] )
+ self.palette.name = colorset.attrib.get('name', "Untitled")
+
+ group = find_group(colorset)
+ if group is None:
+ print(u"Cannot find group by name {}".format(group_name).encode('utf-8'))
+ return None
+ else:
+ self.palette.name = self.palette.name + " - " + group.attrib.get('name', 'Untitled')
+
+ all_slots = []
+ n_colors = 0
+ for xmlclr in group.findall('ColorSetEntry'):
+ name = xmlclr.attrib['name']
+ if xmlclr.attrib['bitdepth'] != 'U8':
+ print("Skip color {}: unsupported bitdepth".format(name))
+ continue
+ rgb = xmlclr.find('RGB')
+ if rgb is None:
+ rgb = xmlclr.find('sRGB')
+ if rgb is None:
+ print("Skip color {}: no RGB representation".format(name))
+ continue
+
+ r = float(rgb.attrib['r'].replace(',', '.'))
+ g = float(rgb.attrib['g'].replace(',', '.'))
+ b = float(rgb.attrib['b'].replace(',', '.'))
+ clr = Color()
+ clr.setRGB1((r,g,b))
+ clr.name = name
+ slot = Slot(clr)
+ slot.mode = USER_DEFINED
+
+ all_slots.append(slot)
+ n_colors += 1
+
+ if n_colors < DEFAULT_GROUP_SIZE:
+ self.palette.ncols = n_colors
+ if not self.palette.ncols:
+ if n_colors > MAX_COLS:
+ self.palette.ncols = MAX_COLS
+ else:
+ self.palette.ncols = n_colors
+ #print("Loaded colors: {}".format(len(all_slots)))
+ self.palette.setSlots(all_slots)
+ self.palette.meta["SourceFormat"] = "KRITA KPL"
+ print("Loaded palette: {}x{}".format( self.palette.nrows, self.palette.ncols ))
+ return self.palette
+
+ def save(self, file_w=None):
+ with ZipFile(file_w, 'w', ZIP_DEFLATED) as zf:
+ zf.writestr("mimetype", MIMETYPE)
+
+ xml = ET.Element("Colorset")
+ xml.attrib['version'] = '1.0'
+ xml.attrib['columns'] = str(self.palette.ncols)
+ xml.attrib['name'] = self.palette.name
+ xml.attrib['comment'] = self.palette.meta.get("Comment", "Generated by Palette Editor")
+
+ for i,row in enumerate(self.palette.slots):
+ for j,slot in enumerate(row):
+ color = slot.color
+ name = color.name
+ default_name = "Swatch-{}-{}".format(i,j)
+ if not name:
+ name = default_name
+
+ elem = ET.SubElement(xml, "ColorSetEntry")
+ elem.attrib['spot'] = color.meta.get("Spot", "false")
+ elem.attrib['id'] = default_name
+ elem.attrib['name'] = name
+ elem.attrib['bitdepth'] = 'U8'
+
+ r,g,b = color.getRGB1()
+ srgb = ET.SubElement(elem, "sRGB")
+ srgb.attrib['r'] = str(r)
+ srgb.attrib['g'] = str(g)
+ srgb.attrib['b'] = str(b)
+
+ tree = ET.ElementTree(xml)
+ tree_str = ET.tostring(tree, encoding='utf-8', pretty_print=True, xml_declaration=False)
+
+ zf.writestr("colorset.xml", tree_str)
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/scribus.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/scribus.py
new file mode 100644
index 0000000..6672b28
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/scribus.py
@@ -0,0 +1,76 @@
+
+from math import floor
+from os.path import join, basename
+from lxml import etree as ET
+
+from color.colors import *
+from color import mixers
+from palette.storage.storage import *
+
+cmyk_factor = float(1.0/255.0)
+
+def fromHex_CMYK(s):
+ t = s[1:]
+ cs,ms,ys,ks = t[0:2], t[2:4], t[4:6], t[6:8]
+ c,m,y,k = int(cs,16), int(ms,16), int(ys,16), int(ks,16)
+ c,m,y,k = [float(x)*cmyk_factor for x in [c,m,y,k]]
+ result = Color()
+ result.setCMYK((c,m,y,k))
+ return result
+
+class Scribus(Storage):
+ name = 'scribus'
+ title = _("Scribus color swatches")
+ filters = ["*.xml"]
+ can_load = True
+ can_save = True
+
+ @staticmethod
+ def check(filename):
+ return ET.parse(filename).getroot().tag == 'SCRIBUSCOLORS'
+
+ def load(self, mixer, file_r, options=None):
+ xml = ET.parse(file_r).getroot()
+
+ colors = []
+ name = xml.attrib.get("Name", "Untitled")
+
+ for elem in xml.findall("COLOR"):
+ if "RGB" in elem.attrib:
+ color = fromHex(elem.attrib["RGB"])
+ colors.append(color)
+ elif "CMYK" in elem.attrib:
+ color = fromHex_CMYK(elem.attrib["CMYK"])
+ colors.append(color)
+ else:
+ continue
+ if "NAME" in elem.attrib:
+ color.meta["Name"] = elem.attrib["NAME"]
+ if "Spot" in elem.attrib:
+ color.meta["Spot"] = elem.attrib["Spot"]
+ if "Register" in elem.attrib:
+ color.meta["Register"] = elem.attrib["Register"]
+
+ self.palette = create_palette(colors)
+ self.palette.name = name
+ return self.palette
+
+ def save(self, file_w):
+ name = self.palette.name
+ if not name:
+ name = "Palette"
+ xml = ET.Element("SCRIBUSCOLORS", NAME=name)
+
+ for i,row in enumerate(self.palette.getColors()):
+ for j,color in enumerate(row):
+ name = color.name
+ if not name:
+ name = "Swatch-{}-{}".format(i,j)
+ elem = ET.SubElement(xml, "COLOR", NAME=name, RGB=color.hex())
+ if "Spot" in color.meta:
+ elem.attrib["Spot"] = color.meta["Spot"]
+ if "Register" in color.meta:
+ elem.attrib["Register"] = color.meta["Register"]
+
+
+ ET.ElementTree(xml).write(file_w, encoding="utf-8", pretty_print=True, xml_declaration=True)
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/storage.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/storage.py
new file mode 100644
index 0000000..26f849b
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/storage.py
@@ -0,0 +1,62 @@
+
+from os.path import join, basename
+from math import sqrt, floor
+
+from color.colors import *
+from color import mixers
+from palette.palette import *
+
+class Storage(object):
+
+ name = None
+ title = None
+ filters = []
+
+ can_load = False
+ can_save = False
+
+ def __init__(self, palette=None):
+ self.palette = palette
+
+ @classmethod
+ def get_filter(cls):
+ return u"{} ({})".format(cls.title, u" ".join(cls.filters))
+
+ @staticmethod
+ def check(filename):
+ return True
+
+ @staticmethod
+ def get_options_widget(dialog, filename):
+ return None
+
+ def load(self, mixer, file_r, *args, **kwargs):
+ raise NotImplemented
+
+ def save(self, file_w):
+ raise NotImplemented
+
+def create_palette(colors, mixer=None, ncols=None):
+ """Create Palette from list of Colors."""
+ if mixer is None:
+ mixer = mixers.MixerRGB
+ palette = Palette(mixer)
+ palette.ncols = ncols
+
+ all_slots = []
+
+ for clr in colors:
+ slot = Slot(clr, user_defined=True)
+ all_slots.append(slot)
+
+ n_colors = len(all_slots)
+
+ if palette.ncols is None:
+ if n_colors > MAX_COLS:
+ palette.ncols = max( int( floor(sqrt(n_colors)) ), 1)
+ else:
+ palette.ncols = n_colors
+
+ palette.setSlots(all_slots)
+ return palette
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/storage/xml.py b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/xml.py
new file mode 100644
index 0000000..47129dc
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/storage/xml.py
@@ -0,0 +1,179 @@
+
+from os.path import join, basename
+from PyQt4 import QtGui
+from lxml import etree as ET
+
+from color.colors import *
+from color import mixers
+from palette.storage.storage import *
+
+class XmlPalette(Storage):
+ name = 'xml'
+ title = _("CREATE palette format")
+ filters = ["*.xml"]
+ can_load = True
+ can_save = True
+
+ @staticmethod
+ def check(filename):
+ return ET.parse(filename).getroot().tag == 'colors'
+
+ @staticmethod
+ def get_options_widget(dialog, filename):
+
+ def on_group_changed(selector):
+ def handler():
+ dialog.options = selector.currentText()
+ dialog.on_current_changed(filename)
+ return handler
+
+ widget = QtGui.QWidget()
+ box = QtGui.QHBoxLayout()
+ label = QtGui.QLabel(_("Group: "))
+ selector = QtGui.QComboBox()
+
+ xml = ET.parse(filename)
+ for xmlgrp in xml.xpath("//group"):
+ xml_label = xmlgrp.find('label')
+ if xml_label is not None:
+ selector.addItem(xml_label.text)
+
+ selector.currentIndexChanged.connect(on_group_changed(selector))
+ selector.setCurrentIndex(0)
+
+ box.addWidget(label)
+ box.addWidget(selector)
+ widget.setLayout(box)
+ return widget
+
+ @staticmethod
+ def get_group_names(filename):
+ result = []
+ xml = ET.parse(filename)
+ for xmlgrp in xml.xpath("//group"):
+ xml_label = xmlgrp.find('label')
+ if xml_label is not None:
+ result.append(xml_label.text)
+ return result
+
+ def save(self, file_w=None):
+ xml = ET.Element("colors")
+ root_group = ET.SubElement(xml, "group")
+ group = ET.SubElement(root_group, "group")
+ label = ET.SubElement(group, "label")
+ label.text = self.palette.name
+ layout = ET.SubElement(group, "layout", columns=str(self.palette.ncols), expanded="True")
+
+ for key,value in self.palette.meta.items():
+ if key != "Name":
+ meta = ET.SubElement(group, "meta", name=key)
+ meta.text = value
+
+ for i,row in enumerate(self.palette.slots):
+ for j,slot in enumerate(row):
+ color = slot.color
+ name = color.name
+ if not name:
+ name = "Swatch-{}-{}".format(i,j)
+ elem = ET.SubElement(group, "color")
+ label = ET.SubElement(elem, "label")
+ label.text = name
+ if slot.mode == USER_DEFINED:
+ meta = ET.SubElement(elem, "meta", name="user_chosen")
+ meta.text = "True"
+ for key,value in color.meta.items():
+ if key != "Name":
+ meta = ET.SubElement(elem, "meta", name=key)
+ meta.text = value
+ rgb = ET.SubElement(elem, "sRGB")
+ r,g,b = color.getRGB1()
+ rgb.attrib["r"] = str(r)
+ rgb.attrib["g"] = str(g)
+ rgb.attrib["b"] = str(b)
+
+ ET.ElementTree(xml).write(file_w, encoding="utf-8", pretty_print=True, xml_declaration=True)
+
+ def load(self, mixer, file_r, group_name):
+
+ def get_label(grp):
+ xml_label = grp.find('label')
+ if xml_label is None:
+ return None
+ return xml_label.text
+
+
+ def find_group(xml):
+ #print("Searching {}".format(group_name))
+ grp = None
+ for xmlgrp in xml.xpath("//group"):
+ label = get_label(xmlgrp)
+ if label is None:
+ continue
+ #print("Found: {}".format(label))
+ if group_name is None or label == group_name:
+ grp = xmlgrp
+ break
+ return grp
+
+ self.palette = Palette(mixer)
+ self.palette.ncols = None
+ xml = ET.parse(file_r)
+ grp = find_group(xml)
+ if grp is None:
+ print(u"Cannot find group by name {}".format(group_name).encode('utf-8'))
+ return None
+ self.palette.name = get_label(grp)
+
+ layout = grp.find('layout')
+ if layout is not None:
+ self.palette.ncols = int( layout.attrib['columns'] )
+
+ metas = grp.findall('meta')
+ if metas is not None:
+ for meta in metas:
+ key = meta.attrib['name']
+ value = meta.text
+ if key != 'Name':
+ self.palette.meta[key] = value
+
+ all_slots = []
+ n_colors = 0
+ for xmlclr in grp.findall('color'):
+ sRGB = xmlclr.find('sRGB')
+ if sRGB is None:
+ continue
+ attrs = sRGB.attrib
+ r = float(attrs['r'].replace(',','.'))
+ g = float(attrs['g'].replace(',','.'))
+ b = float(attrs['b'].replace(',','.'))
+ clr = Color()
+ clr.setRGB1((r,g,b))
+ slot = Slot(clr)
+ metas = xmlclr.findall('meta')
+ if metas is not None:
+ for meta in metas:
+ key = meta.attrib['name']
+ value = meta.text
+ if key == 'user_chosen' and value == 'True':
+ slot.mode = USER_DEFINED
+ else:
+ clr.meta[key] = value
+ label = xmlclr.find('label')
+ if label is not None:
+ clr.name = label.text
+ all_slots.append(slot)
+ n_colors += 1
+
+ if n_colors < DEFAULT_GROUP_SIZE:
+ self.palette.ncols = n_colors
+ if not self.palette.ncols:
+ if n_colors > MAX_COLS:
+ self.palette.ncols = MAX_COLS
+ else:
+ self.palette.ncols = n_colors
+ #print("Loaded colors: {}".format(len(all_slots)))
+ self.palette.setSlots(all_slots)
+ self.palette.meta["SourceFormat"] = "CREATE XML"
+ print("Loaded palette: {}x{}".format( self.palette.nrows, self.palette.ncols ))
+ return self.palette
+
diff --git a/extensions/fablabchemnitz/color_harmony/color_harmony/utils.py b/extensions/fablabchemnitz/color_harmony/color_harmony/utils.py
new file mode 100644
index 0000000..da96e12
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/utils.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2009-2018 Ilya Portnov
+# (original 'palette-editor' tool, version 0.0.7)
+# 2020 Maren Hachmann (extension-ification)
+#
+# 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.
+
+
+# Helper functions
+def circle_hue(i, n, h, max=1.0):
+ h += i * max / n
+ if h > max:
+ h -= max
+ return h
+
+def seq(start, stop, step=1):
+ n = int(round((stop - start)/step))
+ if n > 1:
+ return([start + step*i for i in range(n+1)])
+ else:
+ return([])
+
+def variate(x, step=1.0, dmin=1.0, dmax=None):
+ if dmax is None:
+ dmax = dmin
+ return seq(x-dmin, x+dmax, step)
+
+def clip(x, min=0.0, max=1.0):
+ if x < min:
+ #print("{:.2f} clipped up to {:.2f}".format(x, min))
+ return min
+ if x > max:
+ #print("{:.2f} clipped down to {:.2f}".format(x, max))
+ return max
+ return x
diff --git a/extensions/fablabchemnitz/color_harmony/meta.json b/extensions/fablabchemnitz/color_harmony/meta.json
new file mode 100644
index 0000000..186dd63
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Color Harmony",
+ "id": "fablabchemnitz.de.color_harmony",
+ "path": "color_harmony",
+ "dependent_extensions": null,
+ "original_name": "Color harmony",
+ "original_id": "de.vektorrascheln.color_harmony",
+ "license": "GNU GPL v2",
+ "license_url": "https://gitlab.com/moini_ink/color-harmony/-/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/color_harmony",
+ "fork_url": "https://gitlab.com/moini_ink/color-harmony",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Color+Harmony",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "gitlab.com/moini_ink",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/destructive_clip/destructive_clip.inx b/extensions/fablabchemnitz/destructive_clip/destructive_clip.inx
new file mode 100644
index 0000000..a893183
--- /dev/null
+++ b/extensions/fablabchemnitz/destructive_clip/destructive_clip.inx
@@ -0,0 +1,17 @@
+
+
+ Destructive Clip
+ fablabchemnitz.de.destructive_clip
+
+ path
+
+
+
+
+
+ "Destructively" clip selected paths using the topmost as clipping path
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/destructive_clip/destructive_clip.py b/extensions/fablabchemnitz/destructive_clip/destructive_clip.py
new file mode 100644
index 0000000..554e996
--- /dev/null
+++ b/extensions/fablabchemnitz/destructive_clip/destructive_clip.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+'''
+ ---DESTRUCTIVE Clip---
+ An Inkscape Extension which works like Object|Clip|Set except that the paths clipped are actually *modified*
+ Thus the clipping is included when exported, for example as a DXF file.
+ Select two or more *paths* then choose Extensions|Modify path|Destructive clip. The topmost path will be used to clip the others.
+ Notes:-
+ * Curves in paths are not supported (use Flatten Beziers).
+ * Non-path objects in the selection will be ignored. Use Object|Ungroup.
+ * Paths entirely outside the clipping path will remain untouched (rather than modifying them to an empty path)
+ * Complex paths may take a while (there seems to be no way too show progress)
+ * Yes, using MBR's to do gross clipping might make it faster
+ * No, Python is not my first language (C/C++ is)
+
+ Mark Wilson Feb 2016
+
+ ----
+
+ Edits by Windell H. Oskay, www.evilmadscientit.com, August 2020
+ Update calls to Inkscape 1.0 extension API to avoid deprecation warnings
+ Minimal standardization of python whitespace
+ Handle some errors more gracefully
+
+'''
+
+import inkex
+import sys
+from inkex.paths import Path
+
+
+class DestructiveClip(inkex.EffectExtension):
+
+ def __init__(self):
+ self.tolerance = 0.0001 # arbitrary fudge factor
+ inkex.Effect.__init__(self)
+ self.error_messages = []
+
+ self.curve_error = 'Unable to parse path.\nConsider removing curves with Extensions > Modify Path > Flatten Beziers...'
+
+ def approxEqual(self, a, b):
+ # compare with tiny tolerance
+ return abs(a-b) <= self.tolerance
+
+ def midPoint(self, line):
+ # midPoint of line
+ return [(line[0][0] + line[1][0])/2, (line[0][1] + line[1][1])/2]
+
+ def maxX(self, lineSegments):
+ # return max X coord of lineSegments
+ maxx = 0.0
+ for line in lineSegments:
+ maxx = max(maxx, line[0][0])
+ maxx = max(maxx, line[1][0])
+ return maxx
+
+ def simplepathToLineSegments(self, path):
+ # takes a simplepath and converts to line *segments*, for simplicity.
+ # Thus [MoveTo P0, LineTo P1, LineTo P2] becomes [[P0-P1],[P1,P2]]
+ # only handles, Move, Line and Close.
+ # The simplepath library has already simplified things, normalized relative commands, etc
+ lineSegments = first = prev = this = []
+ errors = set([]) # Similar errors will be stored only once
+ for cmd in path:
+ this = cmd[1]
+ if cmd[0] == 'M': # moveto
+ if first == []:
+ first = this
+ elif cmd[0] == 'L': # lineto
+ lineSegments.append([prev, this])
+ elif cmd[0] == 'Z': # close
+ lineSegments.append([prev, first])
+ first = []
+ elif cmd[0] == 'C':
+ # https://developer.mozilla.org/en/docs/Web/SVG/Tutorial/Paths
+ lineSegments.append([prev, [this[4], this[5]]])
+ errors.add("Curve node detected (svg type C), this node will be handled as a regular node")
+ else:
+ errors.add("Invalid node type detected: {}. This script only handle type M, L, Z".format(cmd[0]))
+ prev = this
+ return (lineSegments, errors)
+
+ def linesgmentsToSimplePath(self, lineSegments):
+ # reverses simplepathToLines - converts line segments to Move/Line-to's
+ path = []
+ end = None
+ for line in lineSegments:
+ start = line[0]
+ if end is None:
+ path.append(['M', start]) # start with a move
+ elif not (self.approxEqual(end[0], start[0]) and self.approxEqual(end[1], start[1])):
+ path.append(['M', start]) # only move if previous end not within tolerance of this start
+ end = line[1]
+ path.append(['L', end])
+ return path
+
+ def lineIntersection(self, L1From, L1To, L2From, L2To):
+ # returns as [x, y] the intersection of the line L1From-L1To and L2From-L2To, or None
+ # http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
+
+ try:
+ dL1 = [L1To[0] - L1From[0], L1To[1] - L1From[1]]
+ dL2 = [L2To[0] - L2From[0], L2To[1] - L2From[1]]
+ except IndexError:
+ inkex.errormsg(self.curve_error)
+ sys.exit()
+
+ denominator = -dL2[0]*dL1[1] + dL1[0]*dL2[1]
+ if not self.approxEqual(denominator, 0.0):
+ s = (-dL1[1]*(L1From[0] - L2From[0]) + dL1[0]*(L1From[1] - L2From[1]))/denominator
+ t = (+dL2[0]*(L1From[1] - L2From[1]) - dL2[1]*(L1From[0] - L2From[0]))/denominator
+ if s >= 0.0 and s <= 1.0 and t >= 0.0 and t <= 1.0:
+ return [L1From[0] + (t * dL1[0]), L1From[1] + (t * dL1[1])]
+ else:
+ return None
+
+ def insideRegion(self, point, lineSegments, lineSegmentsMaxX):
+ # returns true if point is inside the region defined by lineSegments. lineSegmentsMaxX is the maximum X extent
+ ray = [point, [lineSegmentsMaxX*2.0, point[1]]] # hz line to right of point, extending well outside MBR
+ crossings = 0
+ for line in lineSegments:
+ if not self.lineIntersection(line[0], line[1], ray[0], ray[1]) is None:
+ crossings += 1
+ return (crossings % 2) == 1 # odd number of crossings means inside
+
+ def cullSegmentedLine(self, segmentedLine, lineSegments, lineSegmentsMaxX):
+ # returns just the segments in segmentedLine which are inside lineSegments
+ culled = []
+ for segment in segmentedLine:
+ if self.insideRegion(self.midPoint(segment), lineSegments, lineSegmentsMaxX):
+ culled.append(segment)
+ return culled
+
+ def clipLine(self, line, lineSegments):
+ # returns line split where-ever lines in lineSegments cross it
+ linesWrite = [line]
+ for segment in lineSegments:
+ linesRead = linesWrite
+ linesWrite = []
+ for line in linesRead:
+ intersect = self.lineIntersection(line[0], line[1], segment[0], segment[1])
+ if intersect is None:
+ linesWrite.append(line)
+ else: # split
+ linesWrite.append([line[0], intersect])
+ linesWrite.append([intersect, line[1]])
+ return linesWrite
+
+ def clipLineSegments(self, lineSegmentsToClip, clippingLineSegments):
+ # return the lines in lineSegmentsToClip clipped by the lines in clippingLineSegments
+ clippedLines = []
+ for lineToClip in lineSegmentsToClip:
+ clippedLines.extend(self.cullSegmentedLine(self.clipLine(lineToClip, clippingLineSegments), clippingLineSegments, self.maxX(clippingLineSegments)))
+ return clippedLines
+
+ #you can also run the extension Modify Path > To Absolute Coordinates to convert VH to L
+ def fixVHbehaviour(self, elem):
+ raw = Path(elem.get("d")).to_arrays()
+ subpaths, prev = [], 0
+ for i in range(len(raw)): # Breaks compound paths into simple paths
+ if raw[i][0] == 'M' and i != 0:
+ subpaths.append(raw[prev:i])
+ prev = i
+ subpaths.append(raw[prev:])
+ seg = []
+ for simpath in subpaths:
+ if simpath[-1][0] == 'Z':
+ simpath[-1][0] = 'L'
+ if simpath[-2][0] == 'L': simpath[-1][1] = simpath[0][1]
+ else: simpath.pop()
+ for i in range(len(simpath)):
+ if simpath[i][0] == 'V': # vertical and horizontal lines only have one point in args, but 2 are required
+ #inkex.utils.debug(simpath[i][0])
+ simpath[i][0]='L' #overwrite V with regular L command
+ add=simpath[i-1][1][0] #read the X value from previous segment
+ simpath[i][1].append(simpath[i][1][0]) #add the second (missing) argument by taking argument from previous segment
+ simpath[i][1][0]=add #replace with recent X after Y was appended
+ if simpath[i][0] == 'H': # vertical and horizontal lines only have one point in args, but 2 are required
+ #inkex.utils.debug(simpath[i][0])
+ simpath[i][0]='L' #overwrite H with regular L command
+ simpath[i][1].append(simpath[i-1][1][1]) #add the second (missing) argument by taking argument from previous segment
+ #inkex.utils.debug(simpath[i])
+ seg.append(simpath[i])
+ elem.set("d", Path(seg))
+ return seg
+
+ def effect(self):
+ clippingLineSegments = None
+ pathTag = inkex.addNS('path', 'svg')
+ groupTag = inkex.addNS('g', 'svg')
+ self.error_messages = []
+ for id in self.options.ids: # the selection, top-down
+ node = self.svg.selected[id]
+ if node.tag == pathTag:
+ path = self.fixVHbehaviour(node)
+ if clippingLineSegments is None: # first path is the clipper
+ #(clippingLineSegments, errors) = self.simplepathToLineSegments(node.path.to_arrays())
+ (clippingLineSegments, errors) = self.simplepathToLineSegments(path)
+ self.error_messages.extend(['{}: {}'.format(id, err) for err in errors])
+ else:
+ # do all the work!
+ #segmentsToClip, errors = self.simplepathToLineSegments(node.path.to_arrays())
+ segmentsToClip, errors = self.simplepathToLineSegments(path)
+ self.error_messages.extend(['{}: {}'.format(id, err) for err in errors])
+ clippedSegments = self.clipLineSegments(segmentsToClip, clippingLineSegments)
+ if len(clippedSegments) != 0:
+ path = str(inkex.Path(self.linesgmentsToSimplePath(clippedSegments)))
+ node.set('d', path)
+ else:
+ # don't put back an empty path(?) could perhaps put move, move?
+ inkex.errormsg('Object {} clipped to nothing, will not be updated.'.format(node.get('id')))
+ elif node.tag == groupTag: # we don't look inside groups for paths
+ inkex.errormsg('Group object {} will be ignored. Please ungroup before running the script.'.format(id))
+ else: # something else
+ inkex.errormsg('Object {} is not of type path ({}), and will be ignored. Current type "{}".'.format(id, pathTag, node.tag))
+
+ for error in self.error_messages:
+ inkex.errormsg(error)
+
+if __name__ == '__main__':
+ DestructiveClip().run()
diff --git a/extensions/fablabchemnitz/destructive_clip/meta.json b/extensions/fablabchemnitz/destructive_clip/meta.json
new file mode 100644
index 0000000..0bd24d6
--- /dev/null
+++ b/extensions/fablabchemnitz/destructive_clip/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Destructive Clip",
+ "id": "fablabchemnitz.de.destructive_clip",
+ "path": "destructive_clip",
+ "dependent_extensions": null,
+ "original_name": "Destructive Clip",
+ "original_id": "com.funnypolynomial.inkscape.extension.destructiveclip",
+ "license": "MIT License",
+ "license_url": "https://github.com/funnypolynomial/DestructiveClip/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/destructive_clip",
+ "fork_url": "https://github.com/funnypolynomial/DestructiveClip",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Destructive+Clip",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/funnypolynomial",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/dimensioning/dimensioning.inx b/extensions/fablabchemnitz/dimensioning/dimensioning.inx
new file mode 100644
index 0000000..d454db1
--- /dev/null
+++ b/extensions/fablabchemnitz/dimensioning/dimensioning.inx
@@ -0,0 +1,58 @@
+
+
+ Dimensioning (Replaced by LPE)
+ fablabchemnitz.de.dimensioning
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 5
+ 0
+ 100
+ false
+
+
+ 1
+
+
+
+
+
+
+ 0
+
+ true
+
+
+
+
+
+
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/dimensioning/dimensioning.py b/extensions/fablabchemnitz/dimensioning/dimensioning.py
new file mode 100644
index 0000000..4e39429
--- /dev/null
+++ b/extensions/fablabchemnitz/dimensioning/dimensioning.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+'''
+Tool for drawing beautiful DIN-conform dimensioning arrows
+(c) 2012 by Johannes B. Rutzmoser, johannes.rutzmoser (at) googlemail (dot) com
+
+Please contact me, if you know a way how the extension module accepts mouse input; this would help to improve the tool
+
+Add this file and the dimensioning.inx file into the following folder to get the feature run:
+UNIX:
+$HOME/.config/inkscape/extensions/
+
+Mac OS X (when using the binary):
+/Applications/Inkscape.app/Contents/Resources/extensions/
+or
+/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions
+
+WINDOWS (Filepath may differ, depending where the program was installed):
+C:\Program Files\Inkscape\share\extensions
+
+License:
+GNU GENERAL PUBLIC LICENSE
+
+'''
+
+import inkex
+import numpy as np
+import gettext
+_ = gettext.gettext
+from lxml import etree
+from inkex import paths
+
+def norm(a):
+ return a/np.sqrt(np.dot(a, a))
+
+def rotate(tangentvec, point):
+ if tangentvec[0] == 0:
+ angle = - np.pi/2
+ else:
+ angle = np.arctan(tangentvec[1]/tangentvec[0])
+ return 'rotate(' + str(angle/np.pi*180) + ',' + str(point[0]) + ',' + str(point[1]) + ')'
+
+
+class Dimensioning(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--orientation", default='horizontal', help="The type of orientation of the dimensioning (horizontal, vertical or parallel)")
+ pars.add_argument("--arrow_orientation", default='auto', help="The type of orientation of the arrows")
+ pars.add_argument("--line_scale", type=float, default=1.0, help="Scale factor for the line thickness")
+ pars.add_argument("--overlap", type=float, default=1.0, help="Overlap of the helpline over the dimensioning line")
+ pars.add_argument("--distance", type=float, default=1.0, help="Distance of the helpline to the object")
+ pars.add_argument("--position", type=float, default=1.0, help="position of the dimensioning line")
+ pars.add_argument("--flip", type=inkex.Boolean, default=False, help="flip side")
+ pars.add_argument("--scale_factor", type=float, default=1.0, help="scale factor for the dimensioning text")
+ pars.add_argument("--unit", default='px', help="The unit that should be used for the dimensioning")
+ pars.add_argument("--rotate", type=inkex.Boolean, default=True, help="Rotate the annotation?")
+ pars.add_argument("--digit", type=int, default=0, help="number of digits after the point")
+ pars.add_argument("--tab", default="sampling", help="The selected UI-tab when OK was pressed")
+
+ def create_linestyles(self):
+ '''
+ Create the line styles for the drawings.
+ '''
+
+ self.helpline_style = {
+ 'stroke' : '#000000',
+ 'stroke-width' : '{}px'.format(0.5*self.options.line_scale),
+ 'fill' : 'none'
+ }
+ self.dimline_style = {
+ 'stroke' : '#000000',
+ 'stroke-width' : '{}px'.format(0.75*self.options.line_scale),
+ 'fill' : 'none',
+ 'marker-start' : 'url(#ArrowDIN-start)',
+ 'marker-end' : 'url(#ArrowDIN-end)'
+ }
+ self.text_style = {
+ 'font-size' : '{}px'.format(12*self.options.line_scale),
+ 'font-family' : 'Sans',
+ 'font-style' : 'normal',
+ 'text-anchor' : 'middle'
+ }
+ self.helpline_attribs = {'style' : str(inkex.Style(self.helpline_style)),
+ inkex.addNS('label', 'inkscape') : 'helpline',
+ 'd' : 'm 0,0 100,0'
+ }
+ self.text_attribs = {'style' : str(inkex.Style(self.text_style)),
+ 'x' : '100',
+ 'y' : '100'
+ }
+ self.dimline_attribs = {'style' : str(inkex.Style(self.dimline_style)),
+ inkex.addNS('label','inkscape') : 'dimline',
+ 'd' : 'm 0,0 200,0'
+ }
+
+ def effect(self):
+ # will be executed when feature is activated
+ self.create_linestyles()
+ self.makeGroup()
+ self.getPoints()
+ self.calcab()
+ self.drawHelpline()
+ self.drawDimension()
+ self.drawText()
+
+ def makeMarkerstyle(self, name, rotate):
+ defs = self.svg.getElement('/svg:svg//svg:defs')
+ if defs == None:
+ defs = etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg'))
+ marker = etree.SubElement(defs ,inkex.addNS('marker','svg'))
+ marker.set('id', name)
+ marker.set('orient', 'auto')
+ marker.set('refX', '0.0')
+ marker.set('refY', '0.0')
+ marker.set('style', 'overflow:visible')
+ marker.set(inkex.addNS('stockid','inkscape'), name)
+
+ arrow = etree.Element("path")
+ # messy but works; definition of arrows in beautiful DIN-shapes:
+ if name.startswith('ArrowDIN-'):
+ if rotate:
+ arrow.set('d', 'M 8,0 -8,2.11 -8,-2.11 z')
+ else:
+ arrow.set('d', 'M -8,0 8,-2.11 8,2.11 z')
+ if name.startswith('ArrowDINout-'):
+ if rotate:
+ arrow.set('d', 'M 0,0 16,2.11 16,0.5 26,0.5 26,-0.5 16,-0.5 16,-2.11 z')
+ else:
+ arrow.set('d', 'M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z')
+
+ arrow.set('style', 'fill:#000000;stroke:none')
+ marker.append(arrow)
+
+
+ def makeGroup(self):
+ '''puts everything of the dimensioning in a group'''
+ layer = self.svg.get_current_layer()
+ # Group in which the object should be put into
+ grp_name = 'dimensioning'
+ grp_attributes = {inkex.addNS('label', 'inkscape') : grp_name}
+ self.grp = etree.SubElement(layer, 'g', grp_attributes)
+
+ def getPoints(self):
+ self.p1 = np.array([0.,100.])
+ self.p1 = np.array([100.,100.])
+ # Get variables of a selected object
+ for id, node in self.svg.selected.items():
+ # if it is a path:
+ if node.tag == inkex.addNS('path', 'svg'):
+ d = node.get('d')
+ p = paths.CubicSuperPath(d)
+ # p has all nodes with the anchor points in a list;
+ # the rule is [anchorpoint, node, anchorpoint]
+ # the points are lists with x and y coordinate
+ self.p1 = np.array(p[0][0][1])
+ self.p2 = np.array(p[0][-1][1])
+
+
+ def calcab(self):
+ # get p1,p2 ordered for correct dimension direction
+ # determine quadrant
+ if self.p1[0] <= self.p2[0]:
+ if self.p1[1] <= self.p2[1]:
+ quad = 1 # p1 is left,up of p2
+ else: quad = 2 # p1 is left,down of p2
+ elif self.p1[1] <= self.p2[1]:
+ quad = 3 # p1 is right,up of p2
+ else: quad = 4 # p1 is right,down of p2
+ swap = False if quad ==1 else True
+ minp = self.p2 if swap else self.p1
+ maxp = self.p1 if swap else self.p2
+ # distance between points
+ delta = maxp - minp
+ # rotation matrix
+ rotateMat = np.array([[0,-1],[1,0]])
+ # compute the unit vectors e1 and e2 along the cartesian coordinates of the dimension
+ if self.options.orientation == 'horizontal':
+ if quad == 3: self.e1 = np.array([1.0, 0.0])
+ else: self.e1 = np.array([-1.0, 0.0])
+ if self.options.orientation == 'vertical':
+ if quad == 2:
+ self.e1 = np.array([0.0, -1.0])
+ else: self.e1 = np.array([0.0, 1.0])
+ if self.options.orientation == 'parallel':
+ self.e1 = norm(delta)
+ #if quad==2 or quad==3: self.e1 *= -1
+ self.e2 = np.dot(rotateMat, self.e1)
+ if self.options.flip:
+ self.e2 *= -1.
+ # compute the points a and b, where the dimension line arrow spikes start and end
+ dist = self.options.position*self.e2
+ if self.options.flip:
+ outpt = maxp
+ delta *= -1
+ if swap:
+ self.a = outpt + dist
+ self.b = self.a + self.e1*np.dot(self.e1,delta)
+ else:
+ self.b = outpt + dist
+ self.a = self.b + self.e1*np.dot(self.e1,delta)
+ else:
+ outpt = minp
+ if swap:
+ self.b = outpt + dist
+ self.a = self.b + self.e1*np.dot(self.e1,delta)
+ else:
+ self.a = outpt + dist
+ self.b = self.a + self.e1*np.dot(self.e1,delta)
+
+
+ def drawHelpline(self):
+ # manipulate the start- and endpoints with distance and overlap
+ h1_start = self.p1 + norm(self.a - self.p1)*self.options.distance
+ h1_end = self.a + norm(self.a - self.p1)*self.options.overlap
+ h2_start = self.p2 + norm(self.b - self.p2)*self.options.distance
+ h2_end = self.b + norm(self.b - self.p2)*self.options.overlap
+
+ # print the lines
+ hline1 = etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
+ hline1.set('d', 'M %f,%f %f,%f' % (h1_start[0], h1_start[1],h1_end[0],h1_end[1],))
+
+ hline2 = etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
+ hline2.set('d', 'M %f,%f %f,%f' % (h2_start[0], h2_start[1],h2_end[0],h2_end[1],))
+
+ def setMarker(self, option):
+ if option=='inside':
+ # inside
+ self.arrowlen = 6.0 * self.options.line_scale
+ self.dimline_style['marker-start'] = 'url(#ArrowDIN-start)'
+ self.dimline_style['marker-end'] = 'url(#ArrowDIN-end)'
+ self.makeMarkerstyle('ArrowDIN-start', False)
+ self.makeMarkerstyle('ArrowDIN-end', True)
+ else:
+ # outside
+ self.arrowlen = 0
+ self.dimline_style['marker-start'] = 'url(#ArrowDINout-start)'
+ self.dimline_style['marker-end'] = 'url(#ArrowDINout-end)'
+ self.makeMarkerstyle('ArrowDINout-start', False)
+ self.makeMarkerstyle('ArrowDINout-end', True)
+ self.dimline_attribs['style'] = str(inkex.Style(self.dimline_style))
+
+ def drawDimension(self):
+ # critical length, when inside snaps to outside
+ critical_length = 35 * self.options.line_scale
+ if self.options.arrow_orientation == 'auto':
+ if np.abs(np.dot(self.e1, self.b - self.a)) > critical_length:
+ self.setMarker('inside')
+ else:
+ self.setMarker('outside')
+ else:
+ self.setMarker(self.options.arrow_orientation)
+ # start- and endpoint of the dimension line
+ dim_start = self.a + self.arrowlen*norm(self.b - self.a)
+ dim_end = self.b - self.arrowlen*norm(self.b - self.a)
+ # print
+ dimline = etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.dimline_attribs)
+ dimline.set('d', 'M %f,%f %f,%f' % (dim_start[0], dim_start[1], dim_end[0], dim_end[1]))
+
+ def drawText(self):
+ # distance of text to the dimension line
+ self.textdistance = 5.0 * self.options.line_scale
+ if self.e2[1] > 0:
+ textpoint = (self.a + self.b)/2 - self.e2*self.textdistance
+ elif self.e2[1] == 0:
+ textpoint = (self.a + self.b)/2 - np.array([1,0])*self.textdistance
+ else:
+ textpoint = (self.a + self.b)/2 + self.e2*self.textdistance
+
+ value = np.abs(np.dot(self.e1, self.b - self.a)) / (self.svg.unittouu(str(self.options.scale_factor)+self.options.unit))
+ string_value = str(round(value, self.options.digit))
+ # chop off last characters if digit is zero or negative
+ if self.options.digit <=0:
+ string_value = string_value[:-2]
+ text = etree.SubElement(self.grp, inkex.addNS('text', 'svg'), self.text_attribs)
+ # The alternative for framing with dollars, when LATEX Math export is seeked
+ # text.text = '$' + string_value + '$'
+ text.text = string_value
+ text.set('x', str(textpoint[0]))
+ text.set('y', str(textpoint[1]))
+ if self.options.rotate:
+ text.set('transform', rotate(self.e1, textpoint))
+
+if __name__ == '__main__':
+ Dimensioning().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/dimensioning/meta.json b/extensions/fablabchemnitz/dimensioning/meta.json
new file mode 100644
index 0000000..8f5dca6
--- /dev/null
+++ b/extensions/fablabchemnitz/dimensioning/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Dimensioning (Replaced by LPE)",
+ "id": "fablabchemnitz.de.dimensioning",
+ "path": "dimensioning",
+ "dependent_extensions": null,
+ "original_name": "Dimensioning",
+ "original_id": "dimensioning",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/Rutzmoser/inkscape_dimensioning/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/dimensioning",
+ "fork_url": "https://github.com/Rutzmoser/inkscape_dimensioning",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018389",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/Rutzmoser",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.inx b/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.inx
new file mode 100644
index 0000000..7712923
--- /dev/null
+++ b/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.inx
@@ -0,0 +1,16 @@
+
+
+ Duplicate + Reverse + Join
+ fablabchemnitz.de.duplicate_reverse_join
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.py b/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.py
new file mode 100644
index 0000000..56fbc77
--- /dev/null
+++ b/extensions/fablabchemnitz/duplicate_reverse_join/duplicate_reverse_join.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 Ellen Wasboe, ellen@wasbo.net
+#
+# 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.
+"""
+Duplicate subpaths of all selected paths, reverse and join end nodes.
+"""
+
+import inkex
+
+class DuplicateReverseJoin(inkex.EffectExtension):
+
+ def effect(self):
+ for elem in self.svg.selection:
+
+ pp=elem.path.to_absolute()
+ dList=str(pp).split(' M')
+ dFinal=''
+ l=0
+ for sub in dList:
+ if l>0:
+ origSub='M'+dList[l]
+ else:
+ origSub=dList[l]
+
+ elem.path=origSub
+ reSub=elem.path.reverse()
+
+ if l>0:
+ origSub=' '+origSub
+
+ if origSub.find('Z') > -1:
+ dRev=str(reSub).split(' ')
+ strRev=''
+ if dRev[3]=='L' and dRev[1]==dRev[4] and dRev[2]==dRev[5]:
+ strRev=' '.join(dRev[0:3])+' '+' '.join(dRev[6:]) #avoid that reverse path duplicate first node
+ else:
+ strRev=' '.join(dRev)
+ dFinal=dFinal+origSub+' '+strRev #keep original and reverse as separate closed paths
+ if dRev[-1]!='Z':
+ dFinal=dFinal+' Z'#avoid that reverse of closed path is open
+ else:
+ dRev=str(reSub).split(' ')
+ dFinal=dFinal+origSub+' '+' '.join(dRev[3:])+' Z' #pop off M element of reverse path and add Z to close
+ l+=1
+ elem.path=dFinal
+
+
+if __name__ == '__main__':
+ DuplicateReverseJoin().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/duplicate_reverse_join/meta.json b/extensions/fablabchemnitz/duplicate_reverse_join/meta.json
new file mode 100644
index 0000000..496aa83
--- /dev/null
+++ b/extensions/fablabchemnitz/duplicate_reverse_join/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Duplicate + Reverse + Join",
+ "id": "fablabchemnitz.de.duplicate_reverse_join",
+ "path": "duplicate_reverse_join",
+ "dependent_extensions": null,
+ "original_name": "Duplicate, reverse, join",
+ "original_id": "EllenWasbo.cutlings.duplicateReverseJoin",
+ "license": "GNU GPL v2",
+ "license_url": "https://gitlab.com/EllenWasbo/inkscape-extension-duplicatereversejoin/-/blob/master/duplicateReverseJoin.py",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/duplicate_reverse_joi",
+ "fork_url": "https://gitlab.com/EllenWasbo/inkscape-extension-duplicatereversejoin",
+ "documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=120525059",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "gitlab.com/EllenWasbo",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.inx b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.inx
new file mode 100644
index 0000000..35f09b5
--- /dev/null
+++ b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.inx
@@ -0,0 +1,64 @@
+
+
+ Epilog Dashboard BBox Adjust
+ fablabchemnitz.de.epilog_dashboard_bbox_adjust
+
+
+ false
+ 1.0
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../000_about_fablabchemnitz.svg
+
+
+
+ all
+
+
+
+
+
+ Widen the document to send all lines properly to Epilog Dashboard
+
+
+
diff --git a/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.py b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.py
new file mode 100644
index 0000000..7b14afd
--- /dev/null
+++ b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/epilog_dashboard_bbox_adjust.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+
+'''
+Extension for InkScape 1.0
+Features
+- This tool is a helper to adjust the document border including an offset value, which is added.
+Sending vector data to Epilog Dashboard often results in trimmed paths. This leads to wrong geometry where the laser misses to cut them.
+So we add a default (small) amount of 1.0 doc units to expand the document's canvas
+
+Author: Mario Voigt / FabLab Chemnitz
+Mail: mario.voigt@stadtfabrikanten.org
+Date: 21.04.2021
+Last patch: 12.10.2022
+License: GNU GPL v3
+
+#known bugs:
+- viewbox/width/height do not correctly apply if documents only contain an object (not a path). After converting it to path it works. Seems to be a bbox problem
+- note from 07.05.2021: seems if the following order is viewBox/width/height, or width/height/viewBox, the units are not respected. So me mess around a little bit
+
+#Todo
+- add some way to handle translations properly
+
+'''
+
+import math
+import sys
+import inkex
+from inkex import Transform
+sys.path.append("../apply_transformations")
+
+class EpilogDashboardBboxAdjust(inkex.EffectExtension):
+
+ def getElementChildren(self, element, elements = None):
+ if elements == None:
+ elements = []
+ if element.tag != inkex.addNS('g','svg'):
+ elements.append(element)
+ for child in element.getchildren():
+ self.getElementChildren(child, elements)
+ return elements
+
+ def add_arguments(self, pars):
+ pars.add_argument("--tab")
+ pars.add_argument("--apply_transformations", type=inkex.Boolean, default=False, help="Run 'Apply Transformations' extension before running vpype. Helps avoiding geometry shifting")
+ pars.add_argument("--offset", type=float, default="1.0", help="XY Offset (mm) from top left corner")
+ pars.add_argument("--removal", default="none", help="Remove all elements outside the bounding box or selection")
+ pars.add_argument("--use_machine_size", type=inkex.Boolean, default=False, help="Use machine size")
+ pars.add_argument("--machine_size", default="812x508", help="Machine/Size (mm)")
+ pars.add_argument("--debug", type=inkex.Boolean, default=False, help="Debug output")
+ pars.add_argument("--skip_errors", type=inkex.Boolean, default=False, help="Skip on errors")
+
+ def effect(self):
+
+ applyTransformationsAvailable = False # at first we apply external extension
+ try:
+ import apply_transformations
+ applyTransformationsAvailable = True
+ except Exception as e:
+ # self.msg(e)
+ self.msg("Calling 'Apply Transformations' extension failed. Maybe the extension is not installed. You can download it from official InkScape Gallery. Skipping ...")
+
+ if self.options.apply_transformations is True and applyTransformationsAvailable is True:
+ apply_transformations.ApplyTransformations().recursiveFuseTransform(self.document.getroot())
+
+ offset = self.options.offset
+ units = self.svg.unit
+ #https://wiki.inkscape.org/wiki/Units_In_Inkscape
+
+ # create a new bounding box and get the bbox size of all elements of the document (we cannot use the page's bbox)
+ bbox = inkex.BoundingBox()
+ if len(self.svg.selected) > 0:
+ #bbox = self.svg.selection.bounding_box() #it could be so easy! But ...
+ for element in self.svg.selected.values():
+
+ if isinstance (element, inkex.TextElement) or \
+ isinstance (element, inkex.Tspan):
+ if self.options.skip_errors is False:
+ self.msg("Text elements are not supported!")
+ return
+ else:
+ continue
+ else:
+ bbox += element.bounding_box()
+ else:
+ #for element in self.svg.root.getchildren():
+ for element in self.document.getroot().iter("*"):
+ if isinstance (element, inkex.ShapeElement) and element.tag != inkex.addNS('use','svg') and element.get('inkscape:groupmode') != 'layer': #bbox fails for svg:use elements and layers:
+ if isinstance (element, inkex.TextElement) or \
+ isinstance (element, inkex.Tspan):
+ if self.options.skip_errors is False:
+ self.msg("Text elements are not supported!")
+ return
+ else:
+ continue
+ else:
+ bbox += element.bounding_box()
+
+ if abs(bbox.width) == math.inf or abs(bbox.height) == math.inf:
+ inkex.utils.debug("Error while calculating overall bounding box! Check your element types. Things like svg:text or svg:use are not supported. Impossible to continue!")
+ return
+
+ # adjust the viewBox to the bbox size and add the desired offset
+ if self.options.use_machine_size is True:
+ machineWidth = self.svg.unittouu(self.options.machine_size.split('x')[0] + "mm")
+ machineHeight = self.svg.unittouu(self.options.machine_size.split('x')[1] + "mm")
+ width = f'{machineWidth}' + units
+ height = f'{machineHeight}' + units
+ viewBoxXmin = -offset
+ viewBoxYmin = -offset
+ viewBoxXmax = machineWidth
+ viewBoxYmax = machineHeight
+ else:
+ width = f'{bbox.width + offset * 2}' + units
+ height = f'{bbox.height + offset * 2}' + units
+ viewBoxXmin = -offset
+ viewBoxYmin = -offset
+ viewBoxXmax = bbox.width + offset * 2
+ viewBoxYmax = bbox.height + offset * 2
+ self.document.getroot().attrib['width'] = width
+ self.document.getroot().attrib['viewBox'] = f'{viewBoxXmin} {viewBoxYmin} {viewBoxXmax} {viewBoxYmax}'
+ self.document.getroot().attrib['height'] = height
+
+ # translate all elements to fit the adjusted viewBox
+ mat = Transform("translate(%f, %f)" % (-bbox.left,-bbox.top))
+ for element in self.document.getroot().iter("*"):
+ if isinstance (element, inkex.ShapeElement) and element.tag != inkex.addNS('g', 'svg'):
+ element.transform = Transform(mat) @ element.composed_transform()
+
+ if self.options.removal == "outside_canvas":
+ for element in self.document.getroot().iter("*"):
+ if isinstance (element, inkex.ShapeElement) and element.tag != inkex.addNS('g', 'svg'):
+ ebbox = element.bounding_box()
+ #self.msg("{} | bbox: left = {:0.3f} right = {:0.3f} top = {:0.3f} bottom = {:0.3f}".format(element.get('id'), ebbox.left, ebbox.right, ebbox.top, ebbox.bottom))
+ #check if the element's bbox is inside the view canvas. If not: delete it!
+ if ebbox.right < viewBoxXmin or \
+ ebbox.left > viewBoxXmax or \
+ ebbox.top < viewBoxYmin or \
+ ebbox.bottom > viewBoxYmax:
+ if self.options.debug is True:
+ self.msg("Removing {} {}".format(element.get('id'), ebbox))
+ element.delete()
+
+ elif self.options.removal == "outside_selection":
+ if len(self.svg.selected) == 0:
+ inkex.utils.debug("Your selection is empty but you have chosen the option to remove all elements outside selection!")
+ return
+
+ allElements = []
+ for selected in self.svg.selection:
+ allElements = self.getElementChildren(selected, allElements)
+
+ for element in self.document.getroot().iter("*"):
+ if element not in allElements and isinstance (element, inkex.ShapeElement) and element.tag != inkex.addNS('g', 'svg'):
+ if self.options.debug is True:
+ self.msg("Removing {}".format(element.get('id')))
+ element.delete()
+
+if __name__ == '__main__':
+ EpilogDashboardBboxAdjust().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/meta.json b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/meta.json
new file mode 100644
index 0000000..fbc6ea9
--- /dev/null
+++ b/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust/meta.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "Epilog Dashboard BBox Adjust",
+ "id": "fablabchemnitz.de.epilog_dashboard_bbox_adjust",
+ "path": "epilog_dashboard_bbox_adjust",
+ "dependent_extensions": [
+ "apply_transformations"
+ ],
+ "original_name": "Epilog Dashboard BBox Adjust",
+ "original_id": "fablabchemnitz.de.epilog_dashboard_bbox_adjust",
+ "license": "GNU GPL v3",
+ "license_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/LICENSE",
+ "comment": "Written by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/epilog_dashboard_bbox_adjust",
+ "fork_url": null,
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Epilog+Dashboard+BBox+Adjust",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.inx b/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.inx
new file mode 100644
index 0000000..ce30f32
--- /dev/null
+++ b/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.inx
@@ -0,0 +1,20 @@
+
+
+ Frame Animation Sequence
+ fablabchemnitz.de.frame_animation_sequence
+ 0
+ indefinite
+ 9.1
+ true
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.py b/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.py
new file mode 100644
index 0000000..696486a
--- /dev/null
+++ b/extensions/fablabchemnitz/frame_animation_sequence/frame_animation_sequence.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 roberta bennett repeatingshadow@protonmail.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.
+#
+"""
+Create svg path animation as SMIL from frames.
+
+Place each frame of the animation in a layer named 'frame'.
+Each of these layers should have the same number of paths,
+and each path should have the same number of points as the corresponding
+path in other layers.
+Note if there are more than one path in the frames, the Z order of the paths
+must match. It helps to use the XML editor option to observe the z order.
+
+The animation is applied to the paths in the first layer in the sequence, so the
+properties of that layer are used. In particular, the first layer ought to be set
+visible.
+
+Animations with different numbers of frames can be put into different sequences,
+named 'sequence', using sub-groups:
+
+Layers:
+ not_animated_layer1
+ sequence
+ frame
+ path1a
+ path2a
+ frame
+ path1b
+ path2b
+ frame
+ path1c
+ path2c
+ frame
+ path1d
+ path2d
+ sequence
+ frame
+ frame
+ frame
+
+Layer names must contain 'frame' and groups names contain 'sequence',
+eg, frame1 frame 2 frame 30, sequence 1, rythm sequence, rocket sequence
+
+
+"""
+
+import inkex
+
+class AnimateElement(inkex.BaseElement):
+ """animation Elements do not have a visible representation on the canvas"""
+ tag_name = 'animate'
+ @classmethod
+ def new(cls, **attrs):
+ return super().new( **attrs)
+
+
+class AnimationExtension(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--delete", type=inkex.Boolean, help="Remove frames")
+ pars.add_argument("--begin_str", default="0", help="begin string: eg 0;an2.end;an3.begin")
+ pars.add_argument("--repeat_str", default="indefinite", help="indefinite or an integer")
+ pars.add_argument("--dur_str", default="7.9", help="duration in seconds. Do not decorate with units")
+
+
+ def crunchFrames(self,frames):
+ if frames is None:
+ raise inkex.AbortExtension("layer named frame does not exist.")
+ frame0paths = frames[0].findall('svg:path')
+ Dlists = ["{}".format(p.get_path()) for p in frame0paths]
+ for frame in frames[1:]:
+ paths = frame.findall("svg:path")
+ for i, p in enumerate(paths):
+ Dlists[i] += ";\n{}".format(p.get_path())
+ for i, dl in enumerate(Dlists):
+ animel = AnimateElement(
+ attributeName="d",
+ attributeType="XML",
+ begin=self.options.begin_str,
+ dur=self.options.dur_str,
+ repeatCount=self.options.repeat_str,
+ values=dl)
+ frame0paths[i].append(animel)
+ for frame in frames[1:]:
+ if self.options.delete:
+ frame.delete()
+ return
+
+
+ def effect(self):
+ sequences = [ elem for elem in self.svg.findall("svg:g[@inkscape:label]")
+ if "sequence" in (elem.get('inkscape:label'))
+ ]
+ if len(sequences)==0:
+ frames = [ elem for elem in self.svg.findall("svg:g[@inkscape:label]")
+ if "frame" in (elem.get('inkscape:label'))
+ ]
+ self.crunchFrames(frames)
+ return
+ for sequence in sequences:
+ frames = [ elem for elem in sequence.findall("svg:g[@inkscape:label]")
+ if "frame" in (elem.get('inkscape:label'))
+ ]
+ self.crunchFrames(frames)
+
+
+if __name__ == '__main__':
+ AnimationExtension().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/frame_animation_sequence/meta.json b/extensions/fablabchemnitz/frame_animation_sequence/meta.json
new file mode 100644
index 0000000..4b6d85d
--- /dev/null
+++ b/extensions/fablabchemnitz/frame_animation_sequence/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Frame Animation Sequence",
+ "id": "fablabchemnitz.de.frame_animation_sequence",
+ "path": "frame_animation_sequence",
+ "dependent_extensions": null,
+ "original_name": "Animate Extension",
+ "original_id": "org.inkscape.katkitty.animate",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/yttiktak/inkscape_extension_animate/blob/main/AnimationExtension.py",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/frame_animation_sequence",
+ "fork_url": "https://github.com/yttiktak/inkscape_extension_animate",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Frame+Animation+Sequence",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/yttiktak",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.inx b/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.inx
new file mode 100644
index 0000000..31750db
--- /dev/null
+++ b/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.inx
@@ -0,0 +1,20 @@
+
+
+ Guillotine Plus
+ fablabchemnitz.de.guillotine_plus
+ ~/
+ guillotined
+ 300
+ false
+
+ all
+
+
+
+
+
+
+
+
diff --git a/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.py b/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.py
new file mode 100644
index 0000000..b7680b5
--- /dev/null
+++ b/extensions/fablabchemnitz/guillotine_plus/guillotine_plus.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2010 Craig Marshall, craig9 [at] gmail.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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+"""
+This script slices an inkscape drawing along the guides, similarly to
+the GIMP plugin called "guillotine". It can optionally export to the
+same directory as the SVG file with the same name, but with a number
+suffix. e.g.
+
+/home/foo/drawing.svg
+
+will export to:
+
+/home/foo/drawing0.png
+/home/foo/drawing1.png
+/home/foo/drawing2.png
+/home/foo/drawing3.png
+
+etc.
+
+"""
+
+import os
+import locale
+import inkex
+from inkex.command import inkscape
+
+class GuillotinePlus(inkex.EffectExtension):
+
+ """Exports slices made using guides"""
+ def add_arguments(self, pars):
+ pars.add_argument("--directory")
+ pars.add_argument("--image")
+ pars.add_argument("--ignore", type=inkex.Boolean)
+ pars.add_argument("--dpi", type=int)
+
+ def get_all_horizontal_guides(self):
+ """
+ Returns all horizontal guides as a list of floats stored as
+ strings. Each value is the position from 0 in pixels.
+ """
+ for guide in self.svg.namedview.get_guides():
+ if guide.is_horizontal:
+ yield guide.point.y
+
+ def get_all_vertical_guides(self):
+ """
+ Returns all vertical guides as a list of floats stored as
+ strings. Each value is the position from 0 in pixels.
+ """
+ for guide in self.svg.namedview.get_guides():
+ if guide.is_vertical:
+ yield guide.point.x
+
+ def get_horizontal_slice_positions(self):
+ """
+ Make a sorted list of all horizontal guide positions,
+ including 0 and the document height, but not including
+ those outside of the canvas
+ """
+ horizontals = [0.0]
+ height = float(self.svg.viewport_height)
+ for y in self.get_all_horizontal_guides():
+ if 0.0 < y <= height:
+ horizontals.append(y)
+ horizontals.append(height)
+ return sorted(horizontals)
+
+ def get_vertical_slice_positions(self):
+ """
+ Make a sorted list of all vertical guide positions,
+ including 0 and the document width, but not including
+ those outside of the canvas.
+ """
+ verticals = [0.0]
+ width = float(self.svg.viewport_width)
+ for x in self.get_all_vertical_guides():
+ if 0.0 < x <= width:
+ verticals.append(x)
+ verticals.append(width)
+ return sorted(verticals)
+
+ def get_slices(self):
+ """
+ Returns a list of all "slices" as denoted by the guides
+ on the page. Each slice is really just a 4 element list of
+ floats (stored as strings), consisting of the X and Y start
+ position and the X and Y end position.
+ """
+ hs = self.get_horizontal_slice_positions()
+ vs = self.get_vertical_slice_positions()
+ slices = []
+ for i in range(len(hs) - 1):
+ for j in range(len(vs) - 1):
+ slices.append([vs[j], hs[i], vs[j + 1], hs[i + 1]])
+ return slices
+
+ def get_filename_parts(self):
+ """
+ Attempts to get directory and image as passed in by the inkscape
+ dialog. If the boolean ignore flag is set, then it will ignore
+ these settings and try to use the settings from the export
+ filename.
+ """
+
+ if not self.options.ignore:
+ if self.options.image == "" or self.options.image is None:
+ raise inkex.AbortExtension("Please enter an image name")
+ return self.options.directory, self.options.image
+ else:
+ '''
+ First get the export-filename from the document, if the
+ document has been exported before (TODO: Will not work if it
+ hasn't been exported yet), then uses this to return a tuple
+ consisting of the directory to export to, and the filename
+ without extension.
+ '''
+
+ warning = \
+ "To use the export hints option, you " + \
+ "need to have previously exported the document. " + \
+ "Otherwise no export hints exist!"
+
+ try:
+ export_file = self.svg.get('inkscape:export-filename')
+ except KeyError:
+ raise inkex.AbortExtension(warning)
+ if export_file is None:
+ raise inkex.AbortExtension(warning)
+ dirname, filename = os.path.split(export_file)
+ filename = filename.rsplit(".", 1)[0] # Without extension
+ return dirname, filename
+
+ def get_localised_string(self, name):
+ return locale.format_string("%.f", float(name), 0)
+
+ def export_slice(self, sli, filename):
+ """
+ Runs inkscape's command line interface and exports the image
+ slice from the 4 coordinates in s, and saves as the filename
+ given.
+ """
+ coords = ":".join([self.get_localised_string(dim) for dim in sli])
+ inkscape(self.options.input_file, export_dpi=self.options.dpi, export_area=coords, export_filename=filename)
+
+ def export_slices(self, slices):
+ """
+ Takes the slices list and passes each one with a calculated
+ filename/directory into export_slice.
+ """
+ dirname, filename = self.get_filename_parts()
+ # Remove some crusty extensions from name template
+ if filename.endswith('.svg') or filename.endswith('.png'):
+ filename = filename.rsplit('.', 1)[0]
+ if '{' not in filename:
+ filename += '_{}'
+
+ dirname = os.path.abspath(os.path.expanduser(os.path.expandvars(dirname or './')))
+ if not os.path.isdir(dirname):
+ try:
+ os.makedirs(dirname)
+ except IOError:
+ raise inkex.AbortExtension("Target directory could not be created. Change and try again!")
+
+ output_files = []
+ for i, slico in enumerate(slices):
+ fname = os.path.join(dirname, filename.format(i) + '.png')
+ output_files.append(fname)
+ self.export_slice(slico, fname)
+
+ #self.debug("The sliced bitmaps have been saved as:" + "\n\n" + "\n".join(output_files))
+
+ def effect(self):
+ self.export_slices(self.get_slices())
+
+if __name__ == '__main__':
+ GuillotinePlus().run()
diff --git a/extensions/fablabchemnitz/guillotine_plus/meta.json b/extensions/fablabchemnitz/guillotine_plus/meta.json
new file mode 100644
index 0000000..fbbd087
--- /dev/null
+++ b/extensions/fablabchemnitz/guillotine_plus/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Guillotine Plus",
+ "id": "fablabchemnitz.de.guillotine_plus",
+ "path": "guillotine_plus",
+ "dependent_extensions": null,
+ "original_name": "Guillotine Plus",
+ "original_id": "org.inkscape.guillotineplus",
+ "license": "GNU GPL v2",
+ "license_url": "https://inkscape.org/de/~Bootta/%E2%98%85guillotine-plus",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/guillotine_plus",
+ "fork_url": "https://inkscape.org/de/~Bootta/%E2%98%85guillotine-plus",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Guillotine+Plus",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "inkscape.org/Bootta",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/import_attributes/import_attributes.inx b/extensions/fablabchemnitz/import_attributes/import_attributes.inx
new file mode 100644
index 0000000..efd07a4
--- /dev/null
+++ b/extensions/fablabchemnitz/import_attributes/import_attributes.inx
@@ -0,0 +1,20 @@
+
+
+ Import Attributes
+ fablabchemnitz.de.import_attributes
+
+
+
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/import_attributes/import_attributes.py b/extensions/fablabchemnitz/import_attributes/import_attributes.py
new file mode 100644
index 0000000..5ae16d8
--- /dev/null
+++ b/extensions/fablabchemnitz/import_attributes/import_attributes.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+
+import inkex
+import os
+import lxml
+
+class ImportAttributes(inkex.EffectExtension):
+
+ def add_arguments(self, pars):
+ pars.add_argument("--data", default="", help="data file")
+
+ def effect(self):
+
+ if not os.path.exists(self.options.data):
+ self.msg("The input file does not exist. Please select a proper file and try again.")
+ exit(1)
+
+ if os.path.isdir(self.options.data):
+ self.msg("You must specify a file, not a directory!")
+ exit(1)
+
+ with open(self.options.data, 'r') as f:
+ lines = f.read().splitlines()
+ for line in lines:
+ #split on , max 2+1 = 3 items
+ parts = line.split(",", 2)
+ if len(parts) >= 3:
+ id = parts[0]
+ attribute = parts[1]
+ value = parts[2]
+ try:
+ node = self.svg.getElementById(id)
+ if node is not None:
+ try:
+ node.set(attribute, value)
+ except AttributeError:
+ inkex.utils.debug("Unknown Attribute")
+ except AttributeError:
+ inkex.utils.debug("element with id '" + id + "' not found in current selection.")
+ except lxml.etree.XPathEvalError:
+ inkex.utils.debug("invalid input file")
+ exit(1)
+
+if __name__ == '__main__':
+ ImportAttributes().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/import_attributes/meta.json b/extensions/fablabchemnitz/import_attributes/meta.json
new file mode 100644
index 0000000..79c77a2
--- /dev/null
+++ b/extensions/fablabchemnitz/import_attributes/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Import Attributes",
+ "id": "fablabchemnitz.de.import_attributes",
+ "path": "import_attributes",
+ "dependent_extensions": null,
+ "original_name": "Import Attributes",
+ "original_id": "com.mathem.attrib_import.py",
+ "license": "GNU LGPL v2",
+ "license_url": "https://inkscape.org/~MatheM/%E2%98%85simple-attribute-editor+1",
+ "comment": "ported to Inkscape v1 manually by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/import_attributes",
+ "fork_url": "https://inkscape.org/~MatheM/%E2%98%85simple-attribute-editor+1",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Import+Attributes",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "inkscape.org/MatheM",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/isometric_projection/dimetric_projection.inx b/extensions/fablabchemnitz/isometric_projection/dimetric_projection.inx
new file mode 100644
index 0000000..4c3497b
--- /dev/null
+++ b/extensions/fablabchemnitz/isometric_projection/dimetric_projection.inx
@@ -0,0 +1,23 @@
+
+
+ Dimetric Projection
+ fablabchemnitz.de.dimetric_projection
+
+
+
+
+
+ false
+ 15.000
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/isometric_projection/isometric_projection.inx b/extensions/fablabchemnitz/isometric_projection/isometric_projection.inx
new file mode 100644
index 0000000..e18b5f0
--- /dev/null
+++ b/extensions/fablabchemnitz/isometric_projection/isometric_projection.inx
@@ -0,0 +1,23 @@
+
+
+ Isometric Projection
+ fablabchemnitz.de.isometric_projection
+
+
+
+
+
+ false
+ 30.000
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/isometric_projection/isometric_projection.py b/extensions/fablabchemnitz/isometric_projection/isometric_projection.py
new file mode 100644
index 0000000..f23a262
--- /dev/null
+++ b/extensions/fablabchemnitz/isometric_projection/isometric_projection.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+
+import math
+import inkex
+from inkex.transforms import Transform
+
+class IsometricProjection(inkex.EffectExtension):
+ """
+ Convert a flat 2D projection to one of the three visible sides in an
+ isometric projection, and vice versa.
+ """
+
+ attrTransformCenterX = inkex.addNS('transform-center-x', 'inkscape')
+ attrTransformCenterY = inkex.addNS('transform-center-y', 'inkscape')
+
+ # Precomputed values for sine, cosine, and tangent of 30°.
+ rad_30 = math.radians(30)
+ cos_30 = math.cos(rad_30)
+ sin_30 = 0.5 # No point in using math.sin for 30°.
+ tan_30 = math.tan(rad_30)
+
+ # Combined affine transformation matrices. The bottom row of these 3×3
+ # matrices is omitted; it is always [0, 0, 1].
+ transformations = {
+ # From 2D to isometric top down view:
+ # * scale vertically by cos(30°)
+ # * shear horizontally by -30°
+ # * rotate clock-wise 30°
+ 'to_top': [[cos_30, -cos_30, 0],
+ [sin_30, sin_30, 0]],
+
+ # From 2D to isometric left-hand side view:
+ # * scale horizontally by cos(30°)
+ # * shear vertically by -30°
+ 'to_left': [[cos_30, 0, 0],
+ [sin_30, 1, 0]],
+
+ # From 2D to isometric right-hand side view:
+ # * scale horizontally by cos(30°)
+ # * shear vertically by 30°
+ 'to_right': [[cos_30, 0, 0],
+ [-sin_30, 1, 0]],
+
+ # From isometric top down view to 2D:
+ # * rotate counter-clock-wise 30°
+ # * shear horizontally by 30°
+ # * scale vertically by 1 / cos(30°)
+ 'from_top': [[tan_30, 1, 0],
+ [-tan_30, 1, 0]],
+
+ # From isometric left-hand side view to 2D:
+ # * shear vertically by 30°
+ # * scale horizontally by 1 / cos(30°)
+ 'from_left': [[1 / cos_30, 0, 0],
+ [-tan_30, 1, 0]],
+
+ # From isometric right-hand side view to 2D:
+ # * shear vertically by -30°
+ # * scale horizontally by 1 / cos(30°)
+ 'from_right': [[1 / cos_30, 0, 0],
+ [tan_30, 1, 0]]
+ }
+
+ def add_arguments(self, pars):
+ pars.add_argument('--conversion', default='top', help='Conversion to perform: (top|left|right)')
+ pars.add_argument('--reverse', type=inkex.Boolean, default=False, help='Reverse the transformation from isometric projection to flat 2D')
+ self.arg_parser.add_argument('--orthoangle', type=float, default=15.0, help='Isometric angle in degrees')
+
+ def __initConstants(self, angle):
+ # Precomputed values for sine, cosine, and tangent of orthoangle.
+ self.rad = math.radians(angle)
+ self.cos = math.cos(self.rad)
+ self.sin = math.sin(self.rad)
+ self.tan = math.tan(self.rad)
+
+ # Combined affine transformation matrices. The bottom row of these 3×3
+ # matrices is omitted; it is always [0, 0, 1].
+ self.transformations = {
+ # From 2D to isometric top down view:
+ # * scale vertically by cos(∠)
+ # * shear horizontally by -∠
+ # * rotate clock-wise ∠
+ 'to_top': Transform(((self.cos, -self.cos, 0),
+ (self.sin, self.sin, 0))),
+
+ # From 2D to isometric left-hand side view:
+ # * scale horizontally by cos(∠)
+ # * shear vertically by -∠
+ 'to_left': Transform(((self.cos, 0, 0),
+ (self.sin, 1, 0))),
+
+ # From 2D to isometric right-hand side view:
+ # * scale horizontally by cos(∠)
+ # * shear vertically by ∠
+ 'to_right': Transform(((self.cos , 0, 0),
+ (-self.sin, 1, 0))),
+
+ # From isometric top down view to 2D:
+ # * rotate counter-clock-wise orthoangle
+ # * shear horizontally by orthoangle
+ # * scale vertically by 1 / cos(orthoangle)
+ 'from_top': [[self.tan , 1, 0],
+ [-self.tan, 1, 0]],
+
+ # From isometric left-hand side view to 2D:
+ # * shear vertically by orthoangle
+ # * scale horizontally by 1 / cos(orthoangle)
+ 'from_left': [[1 / self.cos, 0, 0],
+ [-self.tan, 1, 0]],
+
+ # From isometric right-hand side view to 2D:
+ # * shear vertically by -orthoangle
+ # * scale horizontally by 1 / cos(orthoangle)
+ 'from_right': [[1 / self.cos, 0, 0],
+ [self.tan, 1, 0]]
+ }
+
+ # The inverse matrices of the above perform the reverse transformations.
+ self.transformations['from_top'] = -self.transformations['to_top']
+ self.transformations['from_left'] = -self.transformations['to_left']
+ self.transformations['from_right'] = -self.transformations['to_right']
+
+ def getTransformCenter(self, midpoint, node):
+ """
+ Find the transformation center of an object. If the user set it
+ manually by dragging it in Inkscape, those coordinates are used.
+ Otherwise, an attempt is made to find the center of the object's
+ bounding box.
+ """
+
+ c_x = node.get(self.attrTransformCenterX)
+ c_y = node.get(self.attrTransformCenterY)
+
+ # Default to dead-center.
+ if c_x is None:
+ c_x = 0.0
+ else:
+ c_x = float(c_x)
+ if c_y is None:
+ c_y = 0.0
+ else:
+ c_y = float(c_y)
+
+ x = midpoint[0] + c_x
+ y = midpoint[1] - c_y
+
+ return [x, y]
+
+ def translateBetweenPoints(self, tr, here, there):
+ """
+ Add a translation to a matrix that moves between two points.
+ """
+
+ x = there[0] - here[0]
+ y = there[1] - here[1]
+ tr.add_translate(x, y)
+
+ def moveTransformationCenter(self, node, midpoint, center_new):
+ """
+ If a transformation center is manually set on the node, move it to
+ match the transformation performed on the node.
+ """
+
+ c_x = node.get(self.attrTransformCenterX)
+ c_y = node.get(self.attrTransformCenterY)
+
+ if c_x is not None:
+ x = str(center_new[0] - midpoint[0])
+ node.set(self.attrTransformCenterX, x)
+ if c_y is not None:
+ y = str(midpoint[1] - center_new[1])
+ node.set(self.attrTransformCenterY, y)
+
+ def effect(self):
+
+ self.__initConstants(self.options.orthoangle)
+
+ if self.options.reverse is True:
+ conversion = "from_" + self.options.conversion
+ else:
+ conversion = "to_" + self.options.conversion
+
+ if len(self.svg.selected) == 0:
+ inkex.errormsg("Please select an object to perform the " +
+ "isometric projection transformation on.")
+ return
+
+ # Default to the flat 2D to isometric top down view conversion if an
+ # invalid identifier is passed.
+ effect_matrix = self.transformations.get(
+ conversion, self.transformations.get('to_top'))
+
+ for id, node in self.svg.selected.items():
+ bbox = node.bounding_box()
+ midpoint = [bbox.center_x, bbox.center_y]
+ center_old = self.getTransformCenter(midpoint, node)
+ transform = Transform(node.get("transform"))
+ # Combine our transformation matrix with any pre-existing transform.
+ tr = transform @ effect_matrix
+
+ # Compute the location of the transformation center after applying
+ # the transformation matrix.
+ center_new = center_old[:]
+ #Transform(matrix).apply_to_point(center_new)
+ tr.apply_to_point(center_new)
+ tr.apply_to_point(midpoint)
+
+ # Add a translation transformation that will move the object to
+ # keep its transformation center in the same place.
+ self.translateBetweenPoints(tr, center_new, center_old)
+
+ node.set('transform', str(tr))
+
+ # Adjust the transformation center.
+ self.moveTransformationCenter(node, midpoint, center_new)
+
+if __name__ == '__main__':
+ IsometricProjection().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/isometric_projection/meta.json b/extensions/fablabchemnitz/isometric_projection/meta.json
new file mode 100644
index 0000000..2739b97
--- /dev/null
+++ b/extensions/fablabchemnitz/isometric_projection/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Isometric Projection",
+ "id": "fablabchemnitz.de.isometric_projection",
+ "path": "isometric_projection",
+ "dependent_extensions": null,
+ "original_name": "Isometric Projection",
+ "original_id": "nl.jeroenhoek.inkscape.filter.isometric_projection_tool",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/jdhoek/inkscape-isometric-projection/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/isometric_projection",
+ "fork_url": "https://github.com/jdhoek/inkscape-isometric-projection",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Isometric+Projection",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/jdhoek",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/low_poly_2/low_poly_2.inx b/extensions/fablabchemnitz/low_poly_2/low_poly_2.inx
new file mode 100644
index 0000000..570f644
--- /dev/null
+++ b/extensions/fablabchemnitz/low_poly_2/low_poly_2.inx
@@ -0,0 +1,16 @@
+
+
+ Low Poly 2
+ fablabchemnitz.de.low_poly_2
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/low_poly_2/low_poly_2.py b/extensions/fablabchemnitz/low_poly_2/low_poly_2.py
new file mode 100644
index 0000000..f61d7c2
--- /dev/null
+++ b/extensions/fablabchemnitz/low_poly_2/low_poly_2.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python3
+
+import os
+import inkex
+import voronoi
+from inkex.transforms import Transform
+from inkex.paths import CubicSuperPath, Path
+from PIL import Image
+from lxml import etree
+import base64
+from io import BytesIO
+import urllib.request as urllib
+
+# A tool for making polygonal art. Can be created with one click with a pass.
+
+class Point:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+ def __lt__(self,other):
+ return (self.x*self.yother.x*other.y)
+
+ def __ge__(self,other):
+ return (self.x*self.y>=other.x*other.y)
+
+ def __eq__(self,other):
+ return (self.x==other.x) and (self.y==other.y)
+
+ def __ne__(self,other):
+ return (self.x!=other.x) or (self.y!=other.y)
+
+ def __str__(self):
+ return "("+str(self.x)+","+str(self.y)+")"
+
+
+class LowPoly2(inkex.Effect):
+ def __init__(self):
+ inkex.Effect.__init__(self)
+
+ # Clipping a line by a bounding box
+ def dot(self, x, y):
+ return x[0] * y[0] + x[1] * y[1]
+
+ def intersectLineSegment(self, line, v1, v2):
+ s1 = self.dot(line, v1) - line[2]
+ s2 = self.dot(line, v2) - line[2]
+ if s1 * s2 > 0:
+ return (0, 0, False)
+ else:
+ tmp = self.dot(line, v1) - self.dot(line, v2)
+ if tmp == 0:
+ return (0, 0, False)
+ u = (line[2] - self.dot(line, v2)) / tmp
+ v = 1 - u
+ return (u * v1[0] + v * v2[0], u * v1[1] + v * v2[1], True)
+
+ def clipEdge(self, vertices, lines, edge, bbox):
+ # bounding box corners
+ bbc = []
+ bbc.append((bbox[0], bbox[2]))
+ bbc.append((bbox[1], bbox[2]))
+ bbc.append((bbox[1], bbox[3]))
+ bbc.append((bbox[0], bbox[3]))
+
+ # record intersections of the line with bounding box edges
+ line = (lines[edge[0]])
+ interpoints = []
+ for i in range(4):
+ p = self.intersectLineSegment(line, bbc[i], bbc[(i + 1) % 4])
+ if (p[2]):
+ interpoints.append(p)
+
+ # if the edge has no intersection, return empty intersection
+ if (len(interpoints) < 2):
+ return []
+
+ if (len(interpoints) > 2): #h appens when the edge crosses the corner of the box
+ interpoints = list(set(interpoints)) # remove doubles
+
+ # points of the edge
+ v1 = vertices[edge[1]]
+ interpoints.append((v1[0], v1[1], False))
+ v2 = vertices[edge[2]]
+ interpoints.append((v2[0], v2[1], False))
+
+ # sorting the points in the widest range to get them in order on the line
+ minx = interpoints[0][0]
+ maxx = interpoints[0][0]
+ miny = interpoints[0][1]
+ maxy = interpoints[0][1]
+ for point in interpoints:
+ minx = min(point[0], minx)
+ maxx = max(point[0], maxx)
+ miny = min(point[1], miny)
+ maxy = max(point[1], maxy)
+
+ if (maxx - minx) > (maxy - miny):
+ interpoints.sort()
+ else:
+ interpoints.sort(key=lambda pt: pt[1])
+
+ start = []
+ inside = False #true when the part of the line studied is in the clip box
+ startWrite = False #true when the part of the line is in the edge segment
+ for point in interpoints:
+ if point[2]: #The point is a bounding box intersection
+ if inside:
+ if startWrite:
+ return [[start[0], start[1]], [point[0], point[1]]]
+ else:
+ return []
+ else:
+ if startWrite:
+ start = point
+ inside = not inside
+ else: # The point is a segment endpoint
+ if startWrite:
+ if inside:
+ # a vertex ends the line inside the bounding box
+ return [[start[0], start[1]], [point[0], point[1]]]
+ else:
+ return []
+ else:
+ if inside:
+ start = point
+ startWrite = not startWrite
+
+ # Transformation helpers
+ def invertTransform(self, mat):
+ det = mat[0][0] * mat[1][1] - mat[0][1] * mat[1][0]
+ if det != 0: #det is 0 only in case of 0 scaling
+ # invert the rotation/scaling part
+ a11 = mat[1][1] / det
+ a12 = -mat[0][1] / det
+ a21 = -mat[1][0] / det
+ a22 = mat[0][0] / det
+
+ # invert the translational part
+ a13 = -(a11 * mat[0][2] + a12 * mat[1][2])
+ a23 = -(a21 * mat[0][2] + a22 * mat[1][2])
+
+ return [[a11, a12, a13], [a21, a22, a23]]
+ else:
+ return [[0, 0, -mat[0][2]], [0, 0, -mat[1][2]]]
+
+ def getGlobalTransform(self, node):
+ parent = node.getparent()
+ myTrans = Transform(node.get('transform')).matrix
+ if myTrans:
+ if parent is not None:
+ parentTrans = self.getGlobalTransform(parent)
+ if parentTrans:
+ return Transform(parentTrans) * Transform(myTrans)
+ else:
+ return myTrans
+ else:
+ if parent is not None:
+ return self.getGlobalTransform(parent)
+ else:
+ return None
+
+ def checkImagePath(self, node):
+ """Embed the data of the selected Image Tag element"""
+ xlink = node.get('xlink:href')
+ if xlink and xlink[:5] == 'data:':
+ # No need, data alread embedded
+ return
+
+ url = urllib.urlparse(xlink)
+ href = urllib.url2pathname(url.path)
+
+ # Primary location always the filename itself.
+ path = self.absolute_href(href or '')
+
+ # Backup directory where we can find the image
+ if not os.path.isfile(path):
+ path = node.get('sodipodi:absref', path)
+
+ if not os.path.isfile(path):
+ inkex.errormsg('File not found "{}". Unable to embed image.').format(path)
+ return
+
+ if (os.path.isfile(path)):
+ return path
+
+ def effect(self):
+ # Check that elements have been selected
+ if len(self.options.ids) == 0:
+ inkex.errormsg("Please select objects!")
+ return
+
+ # Drawing styles
+ linestyle = {
+ 'stroke': '#000000',
+ 'stroke-width': str(self.svg.unittouu('1px')),
+ 'fill': 'none'
+ }
+
+ facestyle = {
+ 'stroke': '#000000',
+ 'stroke-width':'0px',# str(self.svg.unittouu('1px')),
+ 'fill': 'none'
+ }
+
+ # Handle the transformation of the current group
+ parentGroup = (self.svg.selected[self.options.ids[0]]).getparent()
+
+ svg = self.document.getroot()
+ image_element = svg.find('.//{http://www.w3.org/2000/svg}image')
+ if image_element is None:
+ inkex.utils.debug("No image found")
+ exit(1)
+ self.path = self.checkImagePath(image_element) # This also ensures the file exists
+ if self.path is None: # check if image is embedded or linked
+ image_string = image_element.get('{http://www.w3.org/1999/xlink}href')
+ # find comma position
+ i = 0
+ while i < 40:
+ if image_string[i] == ',':
+ break
+ i = i + 1
+ img = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)])))
+ else:
+ img = Image.open(self.path)
+
+ extrinsic_image_width=float(image_element.get('width'))
+ extrinsic_image_height=float(image_element.get('height'))
+ (width, height) = img.size
+ trans = self.getGlobalTransform(parentGroup)
+ invtrans = None
+ if trans:
+ invtrans = self.invertTransform(trans)
+
+ # Recovery of the selected objects
+ pts = []
+ nodes = []
+ seeds = []
+
+ for id in self.options.ids:
+ node = self.svg.selected[id]
+ nodes.append(node)
+ if(node.tag=="{http://www.w3.org/2000/svg}path"):#If it is path
+ # Get vertex coordinates of path
+ points = CubicSuperPath(node.get('d'))
+ for p in points[0]:
+ pt=[p[1][0],p[1][1]]
+ if trans:
+ Transform(trans).apply_to_point(pt)
+ pts.append(Point(pt[0], pt[1]))
+ seeds.append(Point(p[1][0], p[1][1]))
+ else: # For other shapes
+ bbox = node.bounding_box()
+ if bbox:
+ cx = 0.5 * (bbox.left + bbox.top)
+ cy = 0.5 * (bbox.top + bbox.bottom)
+ pt = [cx, cy]
+ if trans:
+ Transform(trans).apply_to_point(pt)
+ pts.append(Point(pt[0], pt[1]))
+ seeds.append(Point(cx, cy))
+ pts.sort()
+ seeds.sort()
+
+ # In Creation of groups to store the result
+ # Delaunay
+ groupDelaunay = etree.SubElement(parentGroup, inkex.addNS('g', 'svg'))
+ groupDelaunay.set(inkex.addNS('label', 'inkscape'), 'Delaunay')
+
+ scale_x=float(extrinsic_image_width)/float(width)
+ scale_y=float(extrinsic_image_height)/float(height)
+ # Voronoi diagram generation
+
+ triangles = voronoi.computeDelaunayTriangulation(seeds)
+ for triangle in triangles:
+ p1 = seeds[triangle[0]]
+ p2 = seeds[triangle[1]]
+ p3 = seeds[triangle[2]]
+ cmds = [['M', [p1.x, p1.y]],
+ ['L', [p2.x, p2.y]],
+ ['L', [p3.x, p3.y]],
+ ['Z', []]]
+ path = etree.Element(inkex.addNS('path', 'svg'))
+ path.set('d', str(Path(cmds)))
+ middleX=(p1.x+p2.x+p3.x)/3.0
+ middleY=(p1.y+p2.y+p3.y)/3.0
+ if width>middleX and height>middleY and middleX>=0 and middleY>=0:
+ pixelColor = img.getpixel((middleX,middleY))
+ facestyle["fill"]=str(inkex.Color((pixelColor[0], pixelColor[1], pixelColor[2])))
+ else:
+ facestyle["fill"]="black"
+ path.set('style', str(inkex.Style(facestyle)))
+ groupDelaunay.append(path)
+
+if __name__ == '__main__':
+ LowPoly2().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/low_poly_2/meta.json b/extensions/fablabchemnitz/low_poly_2/meta.json
new file mode 100644
index 0000000..b32267e
--- /dev/null
+++ b/extensions/fablabchemnitz/low_poly_2/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Low Poly 2",
+ "id": "fablabchemnitz.de.low_poly_2",
+ "path": "low_poly_2",
+ "dependent_extensions": null,
+ "original_name": "★Delauney From Path",
+ "original_id": "miffy.sora.polygonalart",
+ "license": "MIT License",
+ "license_url": "https://github.com/miffysora/Inkscape/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 manually by Mario Voigt; delauney_from_path",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/low_poly_2",
+ "fork_url": "https://github.com/miffysora/Inkscape",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Low+Poly+2",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/miffysora",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/open_in_roland_cutstudio/meta.json b/extensions/fablabchemnitz/open_in_roland_cutstudio/meta.json
new file mode 100644
index 0000000..60d96a0
--- /dev/null
+++ b/extensions/fablabchemnitz/open_in_roland_cutstudio/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Open In Roland CutStudio",
+ "id": "fablabchemnitz.de.open_in_roland_cutstudio",
+ "path": "open_in_roland_cutstudio",
+ "dependent_extensions": null,
+ "original_name": "Open in CutStudio",
+ "original_id": "roland_custudio.export",
+ "license": "GNU GPL v2",
+ "license_url": "https://github.com/mgmax/inkscape-roland-cutstudio/blob/master/roland_cutstudio.py",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/open_in_roland_cutstudio",
+ "fork_url": "https://github.com/mgmax/inkscape-roland-cutstudio",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Open+in+Roland+CutStudio",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/mgmax",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.inx b/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.inx
new file mode 100644
index 0000000..15d3bfd
--- /dev/null
+++ b/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.inx
@@ -0,0 +1,16 @@
+
+
+ Open In Roland CutStudio
+ fablabchemnitz.de.open_in_roland_cutstudio
+
+ path
+
+
+
+
+
+
+
+
diff --git a/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.py b/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.py
new file mode 100644
index 0000000..3cbbecd
--- /dev/null
+++ b/extensions/fablabchemnitz/open_in_roland_cutstudio/open_in_roland_cutstudio.py
@@ -0,0 +1,435 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+'''
+Roland CutStudio export script
+Copyright (C) 2014 - 2020 Max Gaukler
+
+skeleton based on Visicut Inkscape Plugin :
+Copyright (C) 2012 Thomas Oster, thomas.oster@rwth-aachen.de
+
+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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+'''
+
+# The source code is a horrible mess. I apologize for your inconvenience, but hope that it still helps. Feel free to improve :-)
+# Keep everything python2 compatible as long as people out there are using Inkscape <= 0.92.4!
+
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+from __future__ import absolute_import
+
+from builtins import open
+from builtins import map
+from builtins import str
+from builtins import range
+import sys
+import os
+from subprocess import Popen
+import subprocess
+import shutil
+import numpy
+from functools import reduce
+import atexit
+import filecmp
+try:
+ from pathlib import Path
+except ImportError:
+ # Workaround for Python < 3.5
+ class fakepath:
+ def home(self):
+ return os.path.expanduser("~")
+ Path = fakepath()
+import tempfile
+import random
+import string
+
+DEVNULL = open(os.devnull, 'w')
+atexit.register(DEVNULL.close)
+
+def message(s):
+ sys.stderr.write(s+"\n")
+def debug(s):
+ message(s)
+
+# copied from https://github.com/t-oster/VisiCut/blob/0abe785a30d5d5085dd3b5953b38239b1ff83358/tools/inkscape_extension/visicut_export.py
+def which(program, raiseError, extraPaths=[], subdir=None):
+ """
+ find program in the $PATH environment variable and in $extraPaths.
+ If $subdir is given, also look in the given subdirectory of each $PATH entry.
+ """
+ pathlist=os.environ["PATH"].split(os.pathsep)
+ if "nt" in os.name:
+ pathlist.append(os.environ.get("ProgramFiles","C:\Program Files\\"))
+ pathlist.append(os.environ.get("ProgramFiles(x86)","C:\Program Files (x86)\\"))
+ pathlist.append("C:\Program Files\\") # needed for 64bit inkscape on 64bit Win7 machines
+ pathlist.append(os.path.dirname(os.path.dirname(os.getcwd()))) # portable application in the current directory
+ pathlist += extraPaths
+ if subdir:
+ pathlist = [os.path.join(p, subdir) for p in pathlist] + pathlist
+ def is_exe(fpath):
+ return os.path.isfile(fpath) and (os.access(fpath, os.X_OK) or fpath.endswith(".exe"))
+ for path in pathlist:
+ exe_file = os.path.join(path, program)
+ if is_exe(exe_file):
+ return exe_file
+ if raiseError:
+ raise Exception("Cannot find " + str(program) + " in any of these paths: " + str(pathlist) + ". Either the program is not installed, PATH is not set correctly, or this is a bug.")
+ else:
+ return None
+
+# copied from https://github.com/t-oster/VisiCut/blob/0abe785a30d5d5085dd3b5953b38239b1ff83358/tools/inkscape_extension/visicut_export.py
+# Strip SVG to only contain selected elements, convert objects to paths, unlink clones
+# Inkscape version: takes care of special cases where the selected objects depend on non-selected ones.
+# Examples are linked clones, flowtext limited to a shape and linked flowtext boxes (overflow into the next box).
+#
+# Inkscape is called with certain "actions" to do the required cleanup
+# The idea is similar to http://bazaar.launchpad.net/~nikitakit/inkscape/svg2sif/view/head:/share/extensions/synfig_prepare.py#L181 , but more primitive - there is no need for more complicated preprocessing here
+def stripSVG_inkscape(src, dest, elements):
+ # create temporary file for opening with inkscape.
+ # delete this file later so that it will disappear from the "recently opened" list.
+ tmpfile = tempfile.NamedTemporaryFile(delete=False, prefix='temp-visicut-', suffix='.svg')
+ tmpfile.close()
+ tmpfile = tmpfile.name
+ import shutil
+ shutil.copyfile(src, tmpfile)
+
+
+ # Updated for Inkscape 1.2, released 16 May 2022
+ # inkscape --export-overwrite --actions=action1;action2...
+ # (see inkscape --help, inkscape --action-list)
+ # (for debugging, you can look at the intermediate state by running inkscape --with-gui --actions=... my_filename.svg)
+ # Note that it is (almost?) impossible to find a sequence that works in all cases.
+ # Cases to consider:
+ # - selecting whole groups
+ # - selecting objects within a group
+ # - selecting across groups/layers (e.g., enter group, select something, then Shift-click to select things from other layers)
+ # Difficulties with Inkscape:
+ # - "invert selection" does not behave as expected in all these cases,
+ # for example if a group is selected then inverting can select the elements within.
+ # - Inkscape has a wonderful --export-id commandline switch, but it only works correctly with one ID
+
+ # Solution:
+ actions = []
+ # - select objects
+ actions += ["select-by-id:" + ",".join(elements)]
+ # - convert to path
+ actions += ["clone-unlink", "object-to-path"]
+ # - create group of selection
+ actions += ["selection-group"]
+ # - set group ID to a known value. Use a pseudo-random value to avoid collisions
+ target_group_id = "TARGET-GROUP-" + "".join(random.sample(string.ascii_lowercase, 20))
+ actions += ["object-set-attribute:id," + target_group_id]
+ # - set export options: use only the target group ID, nothing else
+ actions += ["export-id-only:true", "export-id:" + target_group_id]
+ # - export
+ actions += ["export-do"]
+
+
+ command = [INKSCAPEBIN, tmpfile, "--export-overwrite", "--actions=" + ";".join(actions)]
+ # to print the resulting commandline:
+ # print(" ".join(["'" + c + "'" for c in command]), file=sys.stderr)
+
+
+ DEBUG = False
+ if DEBUG:
+ # Inkscape sometimes silently ignores wrong verbs, so we need to double-check that everything's right
+ for action in actions:
+ aciton_list = [line.split(":")[0] for line in subprocess.check_output([INKSCAPEBIN, "--action-list"], stderr=DEVNULL).split("\n")]
+ if action not in action_list:
+ sys.stderr.write("Inkscape does not have the action '{}'. Please report this as a VisiCut bug.".format(action))
+
+ inkscape_output = "(not yet run)"
+ try:
+ #sys.stderr.write(" ".join(command))
+ # run inkscape, buffer output
+ inkscape = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ inkscape_output = inkscape.communicate()[0]
+ if inkscape.returncode != 0:
+ sys.stderr.write("Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect.\nInkscape's output was:\n" + str(inkscape_output))
+ except:
+ sys.stderr.write("Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect. Exception information: \n" + str(sys.exc_info()[0]) + "Inkscape's output was:\n" + str(inkscape_output))
+
+ # move output to the intended destination filename
+ os.rename(tmpfile, dest)
+
+
+
+
+# header
+# for debugging purposes you can open the resulting EPS file in Inkscape,
+# select all, ungroup multiple times
+# --> now you can view the exported lines in inkscape
+prefix="""
+%!PS-Adobe-3.0 EPSF-3.0
+%%LanguageLevel: 2
+%%BoundingBox -10000 -10000 10000 10000
+%%EndComments
+%%BeginSetup
+%%EndSetup
+%%BeginProlog
+% This code (until EndProlog) is from an inkscape-exported EPS, copyright unknown, see cairo-library
+save
+50 dict begin
+/q { gsave } bind def
+/Q { grestore } bind def
+/cm { 6 array astore concat } bind def
+/w { setlinewidth } bind def
+/J { setlinecap } bind def
+/j { setlinejoin } bind def
+/M { setmiterlimit } bind def
+/d { setdash } bind def
+/m { moveto } bind def
+/l { lineto } bind def
+/c { curveto } bind def
+/h { closepath } bind def
+/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
+ 0 exch rlineto 0 rlineto closepath } bind def
+/S { stroke } bind def
+/f { fill } bind def
+/f* { eofill } bind def
+/n { newpath } bind def
+/W { clip } bind def
+/W* { eoclip } bind def
+/BT { } bind def
+/ET { } bind def
+/pdfmark where { pop globaldict /?pdfmark /exec load put }
+ { globaldict begin /?pdfmark /pop load def /pdfmark
+ /cleartomark load def end } ifelse
+/BDC { mark 3 1 roll /BDC pdfmark } bind def
+/EMC { mark /EMC pdfmark } bind def
+/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
+/Tj { show currentpoint cairo_store_point } bind def
+/TJ {
+ {
+ dup
+ type /stringtype eq
+ { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
+ } forall
+ currentpoint cairo_store_point
+} bind def
+/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
+ cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
+/Tf { pop /cairo_font exch def /cairo_font_matrix where
+ { pop cairo_selectfont } if } bind def
+/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
+ /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
+ /cairo_font where { pop cairo_selectfont } if } bind def
+/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
+ cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
+/g { setgray } bind def
+/rg { setrgbcolor } bind def
+/d1 { setcachedevice } bind def
+%%EndProlog
+%%Page: 1 1
+%%BeginPageSetup
+%%PageBoundingBox: -10000 -10000 10000 10000
+%%EndPageSetup
+% This is a severely crippled fucked-up pseudo-postscript for importing in Roland CutStudio
+% Do not even try to open it with something else
+% FIXME opening with inkscape currently does not show any objects, although it worked some time in the past
+
+% Inkscape header, not used by cutstudio
+% Start
+q -10000 -10000 10000 10000 rectclip q
+
+0 g
+0.286645 w
+0 J
+0 j
+[] 0.0 d
+4 M q
+% Cutstudio Start
+"""
+postfix="""
+% Cutstudio End
+
+%this is necessary for CutStudio so that the last line isnt skipped:
+0 0 m
+
+% Inkscape footer
+S Q
+Q Q
+showpage
+%%Trailer
+end restore
+%%EOF
+"""
+
+def EPS2CutstudioEPS(src, dest, mirror=False):
+ def outputFromStack(stack, n, transformCoordinates=True):
+ arr=stack[-(n+1):-1]
+ if transformCoordinates:
+ arrTransformed=[]
+ for i in range(n//2):
+ arrTransformed+=transform(arr[2*i], arr[2*i+1])
+ return output(arrTransformed+[stack[-1]])
+ else:
+ return output(arr+[stack[-1]])
+ def transform(x, y):
+ #debug("trafo from: {} {}".format(x, y))
+ p=numpy.array([[float(x),float(y),1]]).transpose()
+ multiply = lambda a, b: numpy.matmul(a, b)
+ # concatenate transformations by multiplying: new = transformation x previousTransformtaion
+ m=reduce(multiply, scalingStack[::-1])
+ m=m.transpose()
+ #debug("with {}".format(m))
+ pnew = numpy.matmul(m, p)
+ x=float(pnew[0])
+ y=float(pnew[1])
+ #debug("to: {} {}".format(x, y))
+ return [x, y]
+ def outputMoveto(x, y):
+ [xx, yy]=transform(x, y)
+ return output([str(xx), str(yy), "m"])
+ def outputLineto(x, y):
+ [xx, yy]=transform(x, y)
+ return output([str(xx), str(yy), "l"])
+ def output(array):
+ array=list(map(str, array))
+ output=" ".join(array)
+ #debug("OUTPUT: "+output)
+ return output + "\n"
+ stack=[]
+ scalingStack=[numpy.identity(3)]
+ if mirror:
+ scalingStack.append(numpy.diag([-1, 1, 1]))
+ lastMoveCoordinates=None
+ outputStr=prefix
+ inputFile=open(src)
+ outputFile=open(dest, "w")
+ for line in inputFile.readlines():
+ line=line.strip()
+ if line.startswith("%"):
+ # comment line
+ continue
+ if line.endswith("re W n"):
+ continue # ignore clipping rectangle
+ #debug(line)
+ for item in line.split(" "):
+ item=item.strip()
+ if item=="":
+ continue
+ #debug("INPUT: " + item.__repr__())
+ stack.append(item)
+ if item=="h": # close path
+ assert lastMoveCoordinates, "closed path before first moveto"
+ outputStr += outputLineto(float(lastMoveCoordinates[0]), float(lastMoveCoordinates[1]))
+ elif item == "c": # bezier curveto
+ outputStr += outputFromStack(stack, 6)
+ stack=[]
+ elif item=="re": # rectangle
+ x=float(stack[-5])
+ y=float(stack[-4])
+ dx=float(stack[-3])
+ dy=float(stack[-2])
+ outputStr += outputMoveto(x, y)
+ outputStr += outputLineto(x+dx, y)
+ outputStr += outputLineto(x+dx, y+dy)
+ outputStr += outputLineto(x, y+dy)
+ outputStr += outputLineto(x, y)
+ elif item=="cm": # matrix transformation
+ newTrafo=numpy.array([[float(stack[-7]), float(stack[-6]), 0], [float(stack[-5]), float(stack[-4]), 0], [float(stack[-3]), float(stack[-2]), 1]])
+ #debug("applying trafo "+str(newTrafo))
+ scalingStack[-1] = numpy.matmul(scalingStack[-1], newTrafo)
+ elif item=="q": # save graphics state to stack
+ scalingStack.append(numpy.identity(3))
+ elif item=="Q": # pop graphics state from stack
+ scalingStack.pop()
+ elif item in ["m", "l"]:
+ if item=="m": # moveto
+ lastMoveCoordinates=stack[-3:-1]
+ elif item=="l": # lineto
+ pass
+ outputStr += outputFromStack(stack, 2)
+ stack=[]
+ else:
+ pass # do nothing
+ outputStr += postfix
+ outputFile.write(outputStr)
+ outputFile.close()
+ inputFile.close()
+
+if os.name=="nt": # windows
+ INKSCAPEBIN = which("inkscape.exe", True, subdir="Inkscape")
+else:
+ INKSCAPEBIN=which("inkscape", True)
+
+assert os.path.isfile(INKSCAPEBIN), "cannot find inkscape binary " + INKSCAPEBIN
+
+selectedElements=[]
+for arg in sys.argv[1:]:
+ if arg[0] == "-":
+ if len(arg) >= 5 and arg[0:5] == "--id=":
+ selectedElements +=[arg[5:]]
+ else:
+ filename = arg
+if "--selftest" in sys.argv:
+ filename = "./test-input.svg"
+
+if len(selectedElements)==0:
+ shutil.copyfile(filename, filename+".filtered.svg")
+else:
+ # only take selected elements
+ stripSVG_inkscape(src=filename, dest=filename+".filtered.svg", elements=selectedElements)
+
+cmd = [INKSCAPEBIN, "-T", "--export-ignore-filters", "--export-area-drawing", "--export-filename="+filename+".inkscape.eps", filename+".filtered.svg"]
+inkscape_eps_file = filename + ".inkscape.eps"
+
+#debug(" ".join(cmd))
+assert 0 == subprocess.call(cmd, stderr=DEVNULL), 'EPS conversion failed: command returned error: ' + '"' + '" "'.join(cmd) + '"'
+assert os.path.exists(inkscape_eps_file), 'EPS conversion failed: command did not create result file: ' + '"' + '" "'.join(cmd) + '"'
+
+
+if "--selftest" in sys.argv:
+ # used for unit-testing: fixed location of output file
+ destination = "./test-output-actual.cutstudio.eps"
+else:
+ # normally
+ destination = filename + ".cutstudio.eps"
+
+EPS2CutstudioEPS(inkscape_eps_file, destination, mirror=("--mirror=true" in sys.argv))
+
+if "--selftest" in sys.argv:
+ # unittest: compare with known reference output
+ TEST_REFERENCE_FILE = "./test-output-reference.cutstudio.eps"
+ assert filecmp.cmp(destination, TEST_REFERENCE_FILE), "Test output changed. Please compare " + destination + " and " + TEST_REFERENCE_FILE
+ print("Selftest successful :-)")
+ sys.exit(0)
+
+if os.name=="nt":
+ DETACHED_PROCESS = 8 # start as "daemon"
+ Popen([which("CutStudio\CutStudio.exe", True), "/import", destination], creationflags=DETACHED_PROCESS, close_fds=True)
+else: #check if we have access to "wine"
+ CUTSTUDIO_C_DRIVE = str(Path.home()) + "/.wine/drive_c/"
+ CUTSTUDIO_PATH_LINUX_WINE = CUTSTUDIO_C_DRIVE + "Program Files (x86)/CutStudio/CutStudio.exe"
+ CUTSTUDIO_COMMANDLINE = ["wine", CUTSTUDIO_PATH_LINUX_WINE, "/import", r'C:\cutstudio.eps']
+ try:
+ if not which("wine", False):
+ raise Exception("Cannot find 'wine'")
+ if not os.path.exists(CUTSTUDIO_PATH_LINUX_WINE):
+ raise Exception("Cannot find CutStudio in " + CUTSTUDIO_PATH_LINUX_WINE)
+ shutil.copyfile(destination, CUTSTUDIO_C_DRIVE + "cutstudio.eps")
+ subprocess.check_call(CUTSTUDIO_COMMANDLINE, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
+ except Exception as exc:
+ message("Could not open CutStudio.\nInstead, your file was saved to:\n" + destination + "\n" + \
+ "Please open that with CutStudio manually. \n\n" + \
+ "Tip: On Linux, you can use 'wine' to install CutStudio 3.10. Then, the file will be directly opened with CutStudio. \n" + \
+ " Diagnostic information: \n" + str(exc))
+ #os.popen("/usr/bin/xdg-open " + filename)
+ #Popen(["inkscape", filename+".filtered.svg"], stderr=DEVNULL)
+ #Popen(["inkscape", filename+".cutstudio.eps"])
+#os.unlink(filename+".filtered.svg")
+#os.unlink(filename)
+#os.unlink(filename+".cutstudio.eps")
diff --git a/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/meta.json b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/meta.json
new file mode 100644
index 0000000..90dafcc
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Optimize Sequence: Small Holes First",
+ "id": "fablabchemnitz.de.optimize_sequence_lasercut_sequence",
+ "path": "optimize_sequence_lasercut_sequence",
+ "dependent_extensions": null,
+ "original_name": "Fix cutting sequence",
+ "original_id": "L0laapk3.filter.lasercut_sequence",
+ "license": "GNU AGPL v3",
+ "license_url": "https://github.com/L0laapk3/inkscape-laser-sequence-extension/blob/master/LICENSE",
+ "comment": "ported to Inkscape v1 by Mario Voigt",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence",
+ "fork_url": "https://github.com/L0laapk3/inkscape-laser-sequence-extension",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Optimize+Sequence%3A+Small+Holes+First",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/L0laapk3",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.inx b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.inx
new file mode 100644
index 0000000..9f53e2b
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.inx
@@ -0,0 +1,16 @@
+
+
+ Optimize Sequence: Small Holes First
+ fablabchemnitz.de.optimize_sequence_lasercut_sequence
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.py b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.py
new file mode 100644
index 0000000..1edf248
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_lasercut_sequence/optimize_sequence_lasercut_sequence.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import inkex
+import measure
+from inkex.paths import Path
+from inkex import paths
+
+def getArea(path):
+ return abs(measure.csparea(paths.CubicSuperPath(path + "z")))
+
+class OptimizeSequenceLasercutSequence(inkex.EffectExtension):
+
+ def effect(self):
+ elements = self.document.xpath('//svg:path',namespaces=inkex.NSS)
+ for el in elements:
+ oldpathstring = el.attrib['d']
+ nodes = Path(oldpathstring).to_arrays()
+ currentSection = []
+ sections = [currentSection]
+ for node in nodes:
+ command = node.pop(0)
+ currentSection.append(command + ' ' + ' '.join(list(map(lambda c: ','.join(map(str, c)), node))))
+ if command.lower() == 'z':
+ currentSection = []
+ sections.append(currentSection)
+
+ sections = list(map(lambda n: ' '.join(n), filter(lambda n: len(n) > 0, sections)))
+
+ if (sections[-1][-2].lower() != 'z'):
+ nonClosedSection = ' ' + sections.pop()
+ else:
+ nonClosedSection = ''
+
+ sections = filter(lambda s: s[0].lower() != 'z', sections)
+ sections = sorted(sections, key=getArea)
+ newpathstring = "z ".join(sections) + nonClosedSection
+ el.set('d', newpathstring)
+
+if __name__ == '__main__':
+ OptimizeSequenceLasercutSequence().run()
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/optimize_sequence_travel_distance/meta.json b/extensions/fablabchemnitz/optimize_sequence_travel_distance/meta.json
new file mode 100644
index 0000000..8074a05
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_travel_distance/meta.json
@@ -0,0 +1,21 @@
+[
+ {
+ "name": "Optimize Sequence: Travel Distances",
+ "id": "fablabchemnitz.de.optimize_sequence_travel_distance",
+ "path": "optimize_sequence_travel_distance",
+ "dependent_extensions": null,
+ "original_name": "Reorder Paths for Speed",
+ "original_id": "command.evilmadscientist.eggbot.reorder",
+ "license": "GNU GPL v3",
+ "license_url": "https://github.com/evil-mad/EggBot/blob/master/LICENSE",
+ "comment": "",
+ "source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/optimize_sequence_travel_distance",
+ "fork_url": "https://github.com/evil-mad/EggBot",
+ "documentation_url": "https://stadtfabrikanten.org/display/IFM/Optimize+Sequence%3A+Travel+Distances",
+ "inkscape_gallery_url": null,
+ "main_authors": [
+ "github.com/evil-mad",
+ "github.com/eridur-de"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.inx b/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.inx
new file mode 100755
index 0000000..6ac06af
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.inx
@@ -0,0 +1,25 @@
+
+
+ Optimize Sequence: Travel Distances
+ fablabchemnitz.de.optimize_sequence_travel_distance
+
+
+
+
+
+
+
+ false
+
+
+ all
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.py b/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.py
new file mode 100644
index 0000000..422417d
--- /dev/null
+++ b/extensions/fablabchemnitz/optimize_sequence_travel_distance/optimize_sequence_travel_distance.py
@@ -0,0 +1,1239 @@
+#!/usr/bin/env python3
+#
+# SVG Path Ordering Extension
+# This extension uses a simple TSP algorithm to order the paths so as
+# to reduce plotting time by plotting nearby paths consecutively.
+#
+#
+# While written from scratch, this is a derivative in spirit of the work by
+# Matthew Beckler and Daniel C. Newman for the EggBot project.
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2020 Windell H. Oskay, Evil Mad Science LLC
+# www.evilmadscientist.com
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import math
+import sys
+from lxml import etree
+import inkex
+import simpletransform
+import simplestyle
+import plot_utils
+
+"""
+TODOs:
+
+* Apparent difference in execution time for portrait vs landscape document orientation.
+ Seems to be related to the _change_
+
+* Implement path functions
+
+
+<_option value=0>Leave as is
+<_option value=1>Reorder subpaths
+<_option value=2>Break apart
+
+
+self.OptionParser.add_option( "--path_handling",\
+action="store", type="int", dest="path_handling",\
+default=1,help="How compound paths are handled")
+
+
+* Consider re-introducing GUI method for rendering:
+
+false
+
+
+"""
+
+class OptimizeSequenceTravelDistance(inkex.EffectExtension):
+ """
+ Inkscape effect extension.
+ Re-order the objects in the SVG document for faster plotting.
+ Respect layers: Initialize a new dictionary of objects for each layer, and sort
+ objects within that layer only
+ Objects in root of document are treated as being on a _single_ layer, and will all
+ be sorted.
+
+ """
+
+ def add_arguments(self, pars):
+ pars.add_argument( "--reordering",type=int, default=1, help="How groups are handled")
+ pars.add_argument( "--preview_rendering",type=inkex.Boolean, default=False, help="Preview rendering") # Rendering is available for debug purposes. It only previews pen-up movements that are reordered and typically does not include all possible movement.
+ self.auto_rotate = True
+
+ def effect(self):
+ # Main entry point of the program
+
+ self.svg_width = 0
+ self.svg_height = 0
+ self.air_total_default = 0
+ self.air_total_sorted = 0
+ self.printPortrait = False
+
+ self.layer_index = 0 # index for coloring layers
+ self.svg = self.document.getroot()
+ self.DocUnits = "in" # Default
+ self.DocUnits = self.svg.unit
+ self.unit_scaling = 1
+ self.getDocProps()
+
+ """
+ Set up the document-wide transforms to handle SVG viewbox
+ """
+
+ matCurrent = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+
+ viewbox = self.svg.get( 'viewBox' )
+
+ vb = self.svg.get('viewBox')
+ if vb:
+ p_a_r = self.svg.get('preserveAspectRatio')
+ sx,sy,ox,oy = plot_utils.vb_scale(vb, p_a_r, self.svg_width, self.svg_height)
+ else:
+ sx = 1.0 / float(plot_utils.PX_PER_INCH) # Handle case of no viewbox
+ sy = sx
+ ox = 0.0
+ oy = 0.0
+
+ # Initial transform of document is based on viewbox, if present:
+ matCurrent = simpletransform.parseTransform('scale({0:.6E},{1:.6E}) translate({2:.6E},{3:.6E})'.format(sx, sy, ox, oy))
+ # Set up x_last, y_last, which keep track of last known pen position
+ # The initial position is given by the expected initial pen position
+
+ self.y_last = 0
+
+ if (self.printPortrait):
+ self.x_last = self.svg_width
+ else:
+ self.x_last = 0
+
+ parent_vis='visible'
+
+ self.root_nodes = []
+
+ if self.options.preview_rendering == True:
+ # Remove old preview layers, if rendering is enabled
+ for node in self.svg:
+ if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
+ if ( node.get( inkex.addNS( 'groupmode', 'inkscape' ) ) == 'layer' ):
+ LayerName = node.get( inkex.addNS( 'label', 'inkscape' ) )
+ if LayerName == '% Preview':
+ self.svg.remove( node )
+
+ preview_transform = simpletransform.parseTransform(
+ 'translate({2:.6E},{3:.6E}) scale({0:.6E},{1:.6E})'.format(
+ 1.0/sx, 1.0/sy, -ox, -oy))
+ path_attrs = { 'transform': simpletransform.formatTransform(preview_transform)}
+ self.preview_layer = etree.Element(inkex.addNS('g', 'svg'),
+ path_attrs, nsmap=inkex.NSS)
+
+
+ self.preview_layer.set( inkex.addNS('groupmode', 'inkscape' ), 'layer' )
+ self.preview_layer.set( inkex.addNS( 'label', 'inkscape' ), '% Preview' )
+ self.svg.append( self.preview_layer )
+
+
+ # Preview stroke width: 1/1000 of page width or height, whichever is smaller
+ if self.svg_width < self.svg_height:
+ width_du = self.svg_width / 1000.0
+ else:
+ width_du = self.svg_height / 1000.0
+
+ """
+ Stroke-width is a css style element, and cannot accept scientific notation.
+
+ Thus, in cases with large scaling (i.e., high values of 1/sx, 1/sy)
+ resulting from the viewbox attribute of the SVG document, it may be necessary to use
+ a _very small_ stroke width, so that the stroke width displayed on the screen
+ has a reasonable width after being displayed greatly magnified thanks to the viewbox.
+
+ Use log10(the number) to determine the scale, and thus the precision needed.
+ """
+
+ log_ten = math.log10(width_du)
+ if log_ten > 0: # For width_du > 1
+ width_string = "{0:.3f}".format(width_du)
+ else:
+ prec = int(math.ceil(-log_ten) + 3)
+ width_string = "{0:.{1}f}".format(width_du, prec)
+
+ self.p_style = {'stroke-width': width_string, 'fill': 'none',
+ 'stroke-linejoin': 'round', 'stroke-linecap': 'round'}
+
+ self.svg = self.parse_svg(self.svg, matCurrent)
+
+
+ def parse_svg(self, input_node, mat_current=None, parent_vis='visible'):
+ """
+ Input: An SVG node (usually) containing other nodes:
+ The SVG root, a layer, sublayer, or other group.
+ Output: The re-ordered node. The contents are reordered with the greedy
+ algorithm, except:
+ - Layers and sublayers are preserved. The contents of each are
+ re-ordered for faster plotting.
+ - Groups are either preserved, broken apart, or re-ordered within
+ the group, depending on the value of group_mode.
+ """
+
+ coord_dict = {}
+ # coord_dict maps a node ID to the following data:
+ # Is the node plottable, first coordinate pair, last coordinate pair.
+ # i.e., Node_id -> (Boolean: plottable, Xi, Yi, Xf, Yf)
+
+ group_dict = {}
+ # group_dict maps a node ID for a group to the contents of that group.
+ # The contents may be a preserved nested group or a flat list, depending
+ # on the selected group handling mode. Example:
+ # group_dict = {'id_1': ,
+ # 'id_2':
+
+ nodes_to_delete = []
+
+ counter = 0 # TODO: Replace this with better unique ID system
+
+ # Account for input_node's transform and any transforms above it:
+ if mat_current is None:
+ mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ try:
+ matNew = simpletransform.composeTransform( mat_current,
+ simpletransform.parseTransform( input_node.get( "transform" )))
+ except AttributeError:
+ matNew = mat_current
+
+ for node in input_node:
+ # Step through each object within the top-level input node
+
+
+ if node.tag is etree.Comment:
+ continue
+
+ try:
+ id = node.get( 'id' )
+ except AttributeError:
+ id = self.svg.get_unique_id("1",True)
+ node.set( 'id', id)
+ if id == None:
+ id = self.svg.get_unique_id("1",True)
+ node.set( 'id', id)
+
+
+ # First check for object visibility:
+ skip_object = False
+
+ # Check for "display:none" in the node's style attribute:
+ style = dict(inkex.Style.parse_str(node.get('style')))
+ if 'display' in style.keys() and style['display'] == 'none':
+ skip_object = True # Plot neither this object nor its children
+
+ # The node may have a display="none" attribute as well:
+ if node.get( 'display' ) == 'none':
+ skip_object = True # Plot neither this object nor its children
+
+ # Visibility attributes control whether a given object will plot.
+ # Children of hidden (not visible) parents may be plotted if
+ # they assert visibility.
+ visibility = node.get( 'visibility', parent_vis )
+
+ if 'visibility' in style.keys():
+ visibility = style['visibility'] # Style may override attribute.
+
+ if visibility == 'inherit':
+ visibility = parent_vis
+
+ if visibility != 'visible':
+ skip_object = True # Skip this object and its children
+
+ # Next, check to see if this inner node is itself a group or layer:
+ if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g':
+
+ # Use the user-given option to decide what to do with subgroups:
+ subgroup_mode = self.options.reordering
+
+# Values of the parameter:
+# subgroup_mode=="1": Preserve groups
+# subgroup_mode=="2": Reorder within groups
+# subgroup_mode=="3": Break apart groups
+
+ if node.get(inkex.addNS('groupmode', 'inkscape')) == 'layer':
+ # The node is a layer or sub-layer, not a regular group.
+ # Parse it separately, and re-order its contents.
+
+ subgroup_mode = 2 # Always sort within each layer.
+ self.layer_index += 1
+
+ layer_name = node.get( inkex.addNS( 'label', 'inkscape' ) )
+
+ if sys.version_info < (3,): # Yes this is ugly. More elegant suggestions welcome. :)
+ layer_name = layer_name.encode( 'ascii', 'ignore' ) #Drop non-ascii characters
+ else:
+ layer_name = str(layer_name)
+ layer_name.lstrip # Remove leading whitespace
+
+ if layer_name:
+ if layer_name[0] == '%': # First character is '%'; This
+ skip_object = True # is a documentation layer; skip plotting.
+ self.layer_index -= 1 # Set this back to previous value.
+
+ if skip_object:
+ # Do not re-order hidden groups or layers.
+ subgroup_mode = 1 # Preserve this group
+
+ if subgroup_mode == 3:
+ # Break apart this non-layer subgroup and add it to
+ # the set of things to be re-ordered.
+
+ nodes_to_delete.append(node)
+ nodes_inside_group = self.group2NodeDict(node)
+
+ for a_node in nodes_inside_group:
+ try:
+ id = a_node.get( 'id' )
+ except AttributeError:
+ id = self.uniqueId("1",True)
+ a_node.set( 'id', id)
+ if id == None:
+ id = self.uniqueId("1",True)
+ a_node.set( 'id', id)
+
+ # Use getFirstPoint and getLastPoint on each object:
+ start_plottable, first_point = self.getFirstPoint(a_node, matNew)
+ end_plottable, last_point = self.getLastPoint(a_node, matNew)
+
+ coord_dict[id] = (start_plottable and end_plottable,
+ first_point[0], first_point[1], last_point[0], last_point[1] )
+ # Entry in group_dict is this node
+ group_dict[id] = a_node
+
+ elif subgroup_mode == 2:
+ # Reorder a layer or subgroup with a recursive call.
+
+ node = self.parse_svg(node, matNew, visibility)
+
+ # Capture the first and last x,y coordinates of the optimized node
+ start_plottable, first_point = self.group_first_pt(node, matNew)
+ end_plottable, last_point = self.group_last_pt(node, matNew)
+
+ # Then add this optimized node to the coord_dict
+ coord_dict[id] = (start_plottable and end_plottable,
+ first_point[0], first_point[1], last_point[0], last_point[1] )
+ # Entry in group_dict is this node
+ group_dict[id] = node
+
+ else: # (subgroup_mode == 1)
+ # Preserve the group, but find its first and last point so
+ # that it can be re-ordered with respect to other items
+
+ if skip_object:
+ start_plottable = False
+ end_plottable = False
+ first_point = [(-1.), (-1.)]
+ last_point = [(-1.), (-1.)]
+ else:
+ start_plottable, first_point = self.group_first_pt(node, matNew)
+ end_plottable, last_point = self.group_last_pt(node, matNew)
+
+ coord_dict[id] = (start_plottable and end_plottable,
+ first_point[0], first_point[1], last_point[0], last_point[1] )
+ # Entry in group_dict is this node
+ group_dict[id] = node
+
+ else: # Handle objects that are not groups
+ if skip_object:
+ start_plottable = False
+ end_plottable = False
+ first_point = [(-1.), (-1.)]
+ last_point = [(-1.), (-1.)]
+ else:
+ start_plottable, first_point = self.getFirstPoint(node, matNew)
+ end_plottable, last_point = self.getLastPoint(node, matNew)
+
+ coord_dict[id] = (start_plottable and end_plottable,
+ first_point[0], first_point[1], last_point[0], last_point[1] )
+ group_dict[id] = node # Entry in group_dict is this node
+
+ # Perform the re-ordering:
+ ordered_element_list = self.ReorderNodeList(coord_dict, group_dict)
+
+ # Once a better order for the svg elements has been determined,
+ # All there is do to is to reintroduce the nodes to the parent in the correct order
+ for elt in ordered_element_list:
+ # Creates identical node at the correct location according to ordered_element_list
+ input_node.append(elt)
+ # Once program is finished parsing through
+ for element_to_remove in nodes_to_delete:
+ try:
+ input_node.remove(element_to_remove)
+ except ValueError:
+ inkex.errormsg(str(element_to_remove.get('id'))+" is not a member of " + str(input_node.get('id')))
+
+ return input_node
+
+
+ def break_apart_path(self, path):
+ """
+ An SVG path may contain multiple distinct portions, that are normally separated
+ by pen-up movements.
+
+ This function takes the path data string from an SVG path, parses it, and returns
+ a dictionary of independent path data strings, each of which represents a single
+ pen-down movement. It is equivalent to the Inkscape function Path > Break Apart
+
+ Input: path data string, representing a single SVG path
+ Output: Dictionary of (separated) path data strings
+
+ """
+ MaxLength = len(path)
+ ix = 0
+ move_to_location = []
+ path_dictionary = {}
+ path_list = []
+ path_number = 1
+
+ # Search for M or m location
+ while ix < MaxLength:
+ if(path[ix] == 'm' or path[ix] == 'M'):
+ move_to_location.append(ix)
+ ix = ix + 1
+ # Iterate through every M or m location in our list of move to instructions
+ # Slice the path string according to path beginning and ends as indicated by the
+ # location of these instructions
+
+ for counter, m in enumerate(move_to_location):
+ if (m == move_to_location[-1]):
+ # last entry
+ path_list.append(path[m:MaxLength].rstrip())
+ else:
+ path_list.append(path[m:move_to_location[counter + 1]].rstrip())
+
+ for counter, current_path in enumerate(path_list):
+
+ # Enumerate over every entry in the path looking for relative m commands
+ if current_path[0] == 'm' and counter > 0:
+ # If path contains relative m command, the best case is when the last command
+ # was a Z or z. In this case, all relative operations are performed relative to
+ # initial x, y coordinates of the previous path
+
+ if path_list[counter -1][-1].upper() == 'Z':
+ current_path_x, current_path_y,index = self.getFirstPoint(current_path, matNew)
+ prev_path_x, prev_path_y,ignore = self.getFirstPoint(path_list[counter-1])
+ adapted_x = current_path_x + prev_path_x
+ adapted_y = current_path_y + prev_path_y
+ # Now we can replace the path data with an Absolute Move to instruction
+ # HOWEVER, we need to adapt all the data until we reach a different command in the case of a repeating
+ path_list[counter] = "m "+str(adapted_x)+","+str(adapted_y) + ' ' +current_path[index:]
+
+ # If there is no z or absolute commands, we need to parse the entire path
+ else:
+
+ # scan path for absolute coordinates. If present, begin parsing from their index
+ # instead of the beginning
+ prev_path = path_list[counter-1]
+ prev_path_length = len(prev_path)
+ jx = 0
+ x_val, y_val = 0,0
+ # Check one char at a time
+ # until we have the moveTo Command
+ last_command = ''
+ is_absolute_command = False
+ repeated_command = False
+ # name of command
+ # how many parameters we need to skip
+ accepted_commands = {
+ 'M':0,
+ 'L':0,
+ 'H':0,
+ 'V':0,
+ 'C':4,
+ 'S':2,
+ 'Q':2,
+ 'T':0,
+ 'A':5
+ }
+
+ # If there is an absolute command which specifies a new initial point
+ # then we can save time by setting our index directly to its location in the path data
+ # See if an accepted_command is present in the path data. If it is present further in the
+ # string than any command found before, then set the pointer to that location
+ # if a command is not found, find() will return a -1. jx is initialized to 0, so if no matches
+ # are found, the program will parse from the beginning to the end of the path
+
+ for keys in 'MLCSQTA': # TODO: Compare to last_point; see if we can clean up this part
+ if(prev_path.find(keys) > jx):
+ jx = prev_path.find(keys)
+
+ while jx < prev_path_length:
+
+ temp_x_val = ''
+ temp_y_val = ''
+ num_of_params_to_skip = 0
+
+ # SVG Path commands can be repeated
+ if (prev_path[jx].isdigit() and last_command):
+ repeated_command = True
+ else:
+ repeated_command = False
+
+ if (prev_path[jx].isalpha() and prev_path[jx].upper() in accepted_commands) or repeated_command:
+
+ if repeated_command:
+ #is_relative_command is saved from last iteration of the loop
+ current_command = last_command
+ else:
+ # If the character is accepted, we must parse until reach the x y coordinates
+ is_absolute_command = prev_path[jx].isupper()
+ current_command = prev_path[jx].upper()
+
+ # Each command has a certain number of parameters we must pass before we reach the
+ # information we care about. We will parse until we know that we have reached them
+
+ # Get to start of next number
+ # We will know we have reached a number if the current character is a +/- sign
+ # or current character is a digit
+ while jx < prev_path_length:
+ if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
+ break
+ jx = jx + 1
+
+ # We need to parse past the unused parameters in our command
+ # The number of parameters to parse past is dependent on the command and stored
+ # as the value of accepted_command
+ # Spaces and commas are used to deliniate paramters
+ while jx < prev_path_length and num_of_params_to_skip < accepted_commands[current_command]:
+ if(prev_path[jx].isspace() or prev_path[jx] == ','):
+ num_of_params_to_skip = num_of_params_to_skip + 1
+ jx = jx + 1
+
+ # Now, we are in front of the x character
+
+ if current_command.upper() == 'V':
+ temp_x_val = 0
+
+ if current_command.upper() == 'H':
+ temp_y_val = 0
+
+ # Parse until next character is a digit or +/- character
+ while jx < prev_path_length and current_command.upper() != 'V':
+ if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
+ break
+ jx = jx + 1
+
+ # Save each next character until we reach a space
+ while jx < prev_path_length and current_command.upper() != 'V' and not (prev_path[jx].isspace() or prev_path[jx] == ','):
+ temp_x_val = temp_x_val + prev_path[jx]
+ jx = jx + 1
+
+ # Then we know we have completely parsed the x character
+
+ # Now we are in front of the y character
+
+ # Parse until next character is a digit or +/- character
+ while jx < prev_path_length and current_command.upper() != 'H':
+ if(prev_path[jx] in '+-' or prev_path[jx].isdigit()):
+ break
+ jx = jx + 1
+
+ ## Save each next character until we reach a space
+ while jx < prev_path_length and current_command.upper() != 'H' and not (prev_path[jx].isspace() or prev_path[jx] == ','):
+ temp_y_val = temp_y_val + prev_path[jx]
+ jx = jx + 1
+
+ # Then we know we have completely parsed the y character
+
+ if is_absolute_command:
+
+ if current_command == 'H':
+ # Absolute commands create new x,y position
+ try:
+ x_val = float(temp_x_val)
+ except ValueError:
+ pass
+ elif current_command == 'V':
+ # Absolute commands create new x,y position
+ try:
+ y_val = float(temp_y_val)
+ except ValueError:
+ pass
+ else:
+ # Absolute commands create new x,y position
+ try:
+ x_val = float(temp_x_val)
+ y_val = float(temp_y_val)
+ except ValueError:
+ pass
+ else:
+
+ if current_command == 'h':
+ # Absolute commands create new x,y position
+ try:
+ x_val = x_val + float(temp_x_val)
+ except ValueError:
+ pass
+ elif current_command == 'V':
+ # Absolute commands create new x,y position
+ try:
+ y_val = y_val + float(temp_y_val)
+ except ValueError:
+ pass
+ else:
+ # Absolute commands create new x,y position
+ try:
+ x_val = x_val + float(temp_x_val)
+ y_val = y_val + float(temp_y_val)
+ except ValueError:
+ pass
+ last_command = current_command
+ jx = jx + 1
+ x,y,index = self.getFirstPoint(current_path,None)
+ path_list[counter] = "m "+str(x_val+x)+","+str(y_val+y) + ' ' + current_path[index:]
+
+ for counter, path in enumerate(path_list):
+ path_dictionary['ad_path'+ str(counter)] = path
+
+ return path_dictionary
+
+
+ def getFirstPoint(self, node, matCurrent):
+ """
+ Input: (non-group) node and parent transformation matrix
+ Output: Boolean value to indicate if the svg element is plottable and
+ two floats stored in a list representing the x and y coordinates we plot first
+ """
+
+ # first apply the current matrix transform to this node's transform
+ matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) )
+
+ point = [float(-1), float(-1)]
+ try:
+ if node.tag == inkex.addNS( 'path', 'svg' ):
+
+ pathdata = node.get('d')
+
+ point = plot_utils.pathdata_first_point(pathdata)
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect':
+
+ """
+ The x,y coordinates for a rect are included in their specific attributes
+ If there is a transform, we need translate the x & y coordinates to their
+ correct location via applyTransformToPoint.
+ """
+
+ point[0] = float( node.get( 'x' ) )
+ point[1] = float( node.get( 'y' ) )
+
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line':
+ """
+ The x1 and y1 attributes are where we will start to draw
+ So, get them, apply the transform matrix, and return the point
+ """
+
+ point[0] = float( node.get( 'x1' ) )
+ point[1] = float( node.get( 'y1' ) )
+
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+
+ if node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline':
+ pl = node.get( 'points', '' ).strip()
+
+ if pl == '':
+ return False, point
+
+ pa = pl.replace(',',' ').split() # replace comma with space before splitting
+
+ if not pa:
+ return False, point
+ pathLength = len( pa )
+ if (pathLength < 4): # Minimum of x1,y1 x2,y2 required.
+ return False, point
+
+ d = "M " + pa[0] + " " + pa[1]
+ i = 2
+ while (i < (pathLength - 1 )):
+ d += " L " + pa[i] + " " + pa[i + 1]
+ i += 2
+
+ point = plot_utils.pathdata_first_point(d)
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if (node.tag == inkex.addNS( 'polygon', 'svg' ) or
+ node.tag == 'polygon'):
+ """
+ We need to extract x1 and y1 from these:
+
+ We accomplish this with Python string strip
+ and split methods. Then apply transforms
+ """
+ # Strip() removes all whitespace from the start and end of p1
+ pl = node.get( 'points', '' ).strip()
+ if (pl == ''):
+ # If pl is blank there has been an error, return False and -1,-1 to indicate a problem has occured
+ return False, point
+ # Split string by whitespace
+ pa = pl.split()
+ if not len( pa ):
+ # If pa is blank there has been an error, return False and -1,-1 to indicate a problem has occured
+ return False, point
+ # pa[0] = "x1,y1
+ # split string via comma to get x1 and y1 individually
+ # then point = [x1,x2]
+ point = pa[0].split(",")
+
+ point = [float(point[0]),float(point[1])]
+
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if node.tag == inkex.addNS( 'ellipse', 'svg' ) or \
+ node.tag == 'ellipse':
+
+ cx = float( node.get( 'cx', '0' ) )
+ cy = float( node.get( 'cy', '0' ) )
+ rx = float( node.get( 'rx', '0' ) )
+
+ point[0] = cx - rx
+ point[1] = cy
+
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if node.tag == inkex.addNS( 'circle', 'svg' ) or \
+ node.tag == 'circle':
+ cx = float( node.get( 'cx', '0' ) )
+ cy = float( node.get( 'cy', '0' ) )
+ r = float( node.get( 'r', '0' ) )
+ point[0] = cx - r
+ point[1] = cy
+
+ simpletransform.applyTransformToPoint(matNew, point)
+
+ return True, point
+
+ if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol':
+ # A symbol is much like a group, except that
+ # it's an invisible object.
+ return False, point # Skip this element.
+
+ if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use':
+
+ """
+ A