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)
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
@ -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)
|
@ -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 "<Slot mode={}>".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
|
||||||
|
|
125
extensions/fablabchemnitz/color_harmony/color_harmony/shades.py
Normal file
125
extensions/fablabchemnitz/color_harmony/color_harmony/shades.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#!/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 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)]
|
@ -0,0 +1 @@
|
|||||||
|
# empty
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,49 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
Reference in New Issue
Block a user