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