diff --git a/extensions/fablabchemnitz/color_harmony/.gitignore b/extensions/fablabchemnitz/color_harmony/.gitignore
new file mode 100644
index 00000000..cd4c22c4
--- /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 00000000..2bab7671
--- /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 00000000..4802b486
--- /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.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 00000000..1bb8bf6d
--- /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 00000000..d9579c77
--- /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 00000000..a391d5fe
--- /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 00000000..0f896806
--- /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 00000000..1bb8bf6d
--- /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 00000000..44745f05
--- /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 00000000..b57d6047
--- /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 00000000..321aa280
--- /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 00000000..a07ee62a
--- /dev/null
+++ b/extensions/fablabchemnitz/color_harmony/color_harmony/matching/transform.py
@@ -0,0 +1,243 @@
+#!/usr/bin/python
+
+# -*- coding: utf-8 -*-
+
+# -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 00000000..dd795fd0
--- /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 00000000..698773c5
--- /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 00000000..7f2a9f1e
--- /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 00000000..1bb8bf6d
--- /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 00000000..8ff0c817
--- /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 00000000..9936f453
--- /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 00000000..f82b3302
--- /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 00000000..5178e939
--- /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 00000000..275b2be3
--- /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 00000000..6672b28f
--- /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 00000000..26f849bd
--- /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 00000000..47129dca
--- /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 00000000..da96e127
--- /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