added color harmony extension

This commit is contained in:
Mario Voigt 2021-04-25 23:48:09 +02:00
parent 8386b76eda
commit 3abb77ea77
25 changed files with 3193 additions and 0 deletions

View File

@ -0,0 +1 @@
*__pycache__*

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Color Harmony</name>
<id>fablabchemnitz.de.color_harmony</id>
<param name="tab" type="notebook">
<page name="render" gui-text="Create Color Harmony">
<label>Select an object that is filled with the color that you want to use as a base for your palette.</label>
<param name="harmony" type="optiongroup" appearance="combo" gui-text="Color Harmony:" gui-description="The asterisk means that the Angle modificator parameter can be used to change the outcome.">
<option value="just_opposite">Just opposite</option>
<option value="split_complementary">Split complementary *</option>
<option value="three">Three colors</option>
<option value="four">Four colors</option>
<option value="rectangle">Rectangle *</option>
<option value="five">Five colors *</option>
<option value="similar_3">Three similar colors *</option>
<option value="similar_5">Five similar colors *</option>
<option value="similar_and_opposite">Similar and opposite *</option>
<option value="from_raster">From selected raster image</option>
</param>
<param name="factor" type="int" min="1" max="100" gui-text="Angle modificator *" appearance="full" gui-description="Factor for determining the angle on the color circle for some of the harmonies (those that are marked with an asterisk)">50</param>
<param name="sort" type="optiongroup" appearance="combo" gui-text="Sort by:">
<option value="by_hue">Hue, 0-360°</option>
<option value="hue_contiguous">Hue, start from largest gap</option>
<option value="by_saturation">Saturation</option>
<option value="by_value">Value</option>
</param>
<label appearance="header">Add shades</label>
<hbox>
<vbox>
<param type="bool" name="cooler" gui-text="Cooler">false</param>
<param type="bool" name="warmer" gui-text="Warmer">false</param>
<param type="bool" name="saturation" gui-text="Saturation">false</param>
</vbox>
<vbox>
<param type="bool" name="value" gui-text="Value">false</param>
<param type="bool" name="chroma" gui-text="Chroma">false</param>
<param type="bool" name="luma" gui-text="Luma">false</param>
</vbox>
<vbox>
<param type="bool" name="hue" gui-text="Hue">false</param>
<param type="bool" name="hue_luma" gui-text="Hue / Luma">false</param>
<param type="bool" name="luma_plus_chroma" gui-text="Luma plus Chroma">false</param>
</vbox>
<vbox>
<param type="bool" name="luma_minus_chroma" gui-text="Luma minus Chroma">false</param>
</vbox>
</hbox>
<param name="step_width" type="float" min="0" max="1" gui-text="Shading step width:">0.1</param>
<label appearance="header">Size</label>
<param name="size" type="int" min="0" max="10000" gui-text="Size:">10</param>
<param name="unit" type="optiongroup" appearance="combo" gui-text="Units:">
<option value="mm">mm</option>
<option value="cm">cm</option>
<option value="in">in</option>
<option value="px">px</option>
<option value="pt">pt</option>
<option value="pc">pc</option>
</param>
<param name="delete_existing" type="boolean" gui-text="Remove old palettes">true</param>
</page>
<page name="save" gui-text="Save as Palette File">
<label>Save all selected palettes to a (single) palette file</label>
<param name="palette_format" type="optiongroup" appearance="combo" gui-text="Palette file format:">
<option value="gimp">Gimp Palette (.gpl)</option>
<option value="krita">Krita Palette (.kpl)</option>
<option value="scribus">Scribus Palette (.xml)</option>
</param>
<param type="path" name="palette_folder" gui-text="Folder to save palette file:" mode="folder" />
<param name="palette_name" type="string" gui-text="Palette name">My Palette</param>
</page>
<page name="colorize" gui-text="Magic Colors">
<label>Press "Apply" to colorize the selection with the rendered palette.</label>
</page>
</param>
<effect needs-live-preview="true">
<object-type>all</object-type>
<menu-tip>Generate color harmonies and save as palette file</menu-tip>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Various"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">color_harmony.py</command>
</script>
</inkscape-extension>

View File

@ -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 <portnov84@rambler.ru>
# (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()

View File

@ -0,0 +1 @@
# empty

View File

@ -0,0 +1,260 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (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 <andrewc-git@piffle.org>
#
# 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 0r1, 0g1, 0b1.
:rtype: tuple (h, c, y) where 0h<1, but 0c2 and 0y1.
"""
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 0h<1, but 0c2 and 0y1.
: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<r)
th = 2.0 - h
tm = _HCY_GREEN_LUMA + _HCY_RED_LUMA * th
elif h < 3:
#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 = 4.0 - h
tm = _HCY_BLUE_LUMA + _HCY_GREEN_LUMA * th
elif h < 5:
#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<b)
th = 6.0 - h
tm = _HCY_RED_LUMA + _HCY_BLUE_LUMA * th
# Calculate the RGB components in sorted order
if tm >= 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

View File

@ -0,0 +1,139 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (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

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (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]]

View File

@ -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

View File

@ -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

View File

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

View File

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