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