added color harmony extension
This commit is contained in:
parent
8386b76eda
commit
3abb77ea77
1
extensions/fablabchemnitz/color_harmony/.gitignore
vendored
Normal file
1
extensions/fablabchemnitz/color_harmony/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*__pycache__*
|
87
extensions/fablabchemnitz/color_harmony/color_harmony.inx
Normal file
87
extensions/fablabchemnitz/color_harmony/color_harmony.inx
Normal 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>
|
318
extensions/fablabchemnitz/color_harmony/color_harmony.py
Normal file
318
extensions/fablabchemnitz/color_harmony/color_harmony.py
Normal 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()
|
@ -0,0 +1 @@
|
||||
# empty
|
@ -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 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<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
|
139
extensions/fablabchemnitz/color_harmony/color_harmony/export.py
Normal file
139
extensions/fablabchemnitz/color_harmony/color_harmony/export.py
Normal 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
|
@ -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]]
|
@ -0,0 +1 @@
|
||||
# empty
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||