added color harmony extension

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

View File

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

View File

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

View File

@ -0,0 +1,318 @@
#!/usr/bin/env python3
# Color Harmony - Inkscape extension to generate
# palettes of colors that go well together
#
# Version 0.2 "golem"
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (original 'palette-editor' tool, version 0.0.7)
# 2020 Maren Hachmann (extension-ification)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""
This extension allows you to automatically add guides to your Inkscape documents.
"""
#from math import sqrt
#import types
import inkex
from inkex import Group, Rectangle
from inkex.colors import is_color
from color_harmony.colorplus import ColorPlus
from color_harmony.harmonies import *
from color_harmony.shades import *
class ColorHarmony(inkex.EffectExtension):
"""Generate palettes of colors that go well together"""
color = ''
def add_arguments(self, pars):
# General options
pars.add_argument('--tab', default='render', help='Extension functionality to use') # options: render, save, colorize
# Render tab options
pars.add_argument('--harmony', default="five", help='Color harmony to generate, options: from_raster, just_opposite, split_complementary, three, four, rectangle, five, similar_3, similar_5, similar_and_opposite')
pars.add_argument('--sort', default="by_hue", help="Method to sort the palette by, options: by_hue, by_saturation, by_value, hue_contiguous")
pars.add_argument('--factor', default=50, type=int, help="Factor to affect the result, between 1 and 100. Default is 50. This modifies the angle between the resulting colors on the color wheel.")
pars.add_argument('--size', default=10, help="Size of the generated palette squares")
pars.add_argument('--unit', default='mm', help='Units') # options: mm, cm, in, px, pt, pc
pars.add_argument('--delete_existing', type=inkex.Boolean, help='Delete existing palettes before generating a new one')
# Shading: cooler, warmer, saturation, value, chroma, luma, hue, hue_luma, luma_plus_chroma, luma_minus_chroma
pars.add_argument( '--cooler', type=inkex.Boolean, help='Add shades with cooler color temperature')
pars.add_argument( '--warmer', type=inkex.Boolean, help='Add shades with warmer color temperature')
pars.add_argument( '--saturation', type=inkex.Boolean, help='Add shades with saturation steps')
pars.add_argument( '--value', type=inkex.Boolean, help='Add shades with value steps')
pars.add_argument( '--chroma', type=inkex.Boolean, help='Add shades with chroma steps')
pars.add_argument( '--luma', type=inkex.Boolean, help='Add shades with luma steps')
pars.add_argument( '--hue', type=inkex.Boolean, help='Add shades with hue steps')
pars.add_argument( '--hue_luma', type=inkex.Boolean, help='Add shades with hue and luma steps')
pars.add_argument( '--luma_plus_chroma', type=inkex.Boolean, help='Add shades with luma plus chroma steps')
pars.add_argument( '--luma_minus_chroma', type=inkex.Boolean, help='Add shades with luma minus chroma steps')
pars.add_argument('--step_width', type=float, default=0.1, help='Shader step width') # TODO: find out what range this can take on, and adjust min, max, default in inx
# Save tab options
pars.add_argument('--palette_format', default='gimp', help='Palette file format')
# options: gimp, krita, scribus
pars.add_argument('--palette_folder', help="Folder to save the palette file in")
pars.add_argument('--palette_name', help="Name of the palette")
# Colorize tab options
# no options currently
def effect(self):
if self.options.tab == "render":
if len(self.svg.selected) == 1:
for obj_id, obj in self.svg.selected.items():
fill = obj.style.get("fill")
if is_color(fill):
if self.options.delete_existing:
self.delete_existing_palettes()
self.color = ColorPlus(fill)
self.options.factor = self.options.factor/100
colors = self.create_harmony()
shades = self.create_shades(colors)
palettes = [colors] + shades
for i in range(len(palettes)):
self.render_palette(palettes[i], i)
else:
raise inkex.AbortExtension(
"Please select an object with a plain fill color.")
else:
raise inkex.AbortExtension(
"Please select one object.")
elif self.options.tab == "save":
palettes = self.get_palettes_in_doc()
if len(palettes) >= 1:
self.save_palette(palettes[0])
else:
raise inkex.AbortExtension(
"There is no rendered palette in the document. Please render a palette using the first tab of the dialog before you try to save it.")
elif self.options.tab == "colorize":
if len(self.svg.selected) > 0:
self.colorize()
else:
raise inkex.AbortExtension(
"Please select an object to colorize!")
# METHODS FOR EACH TAB
# --------------------
# Render tab
# ==========
def create_harmony(self):
harmony_functions = {
"from_raster": self.palette_from_raster, # not implemented yet
"just_opposite": self.opposite,
"split_complementary": self.splitcomplementary,
"three": self.nhues3,
"four": self.nhues4,
"rectangle": self.rectangle,
"five": self.fivecolors,
"similar_3": self.similar_3,
"similar_5": self.similar_5,
"similar_and_opposite": self.similaropposite,
}
# use appropriate function for the selected tab
colors = harmony_functions[self.options.harmony](self.color)
colors = self.sort_colors(colors)
return colors
def render_palette(self, colors, shift):
size = self.svg.unittouu(str(self.options.size)+self.options.unit)
top = 0 + shift * size
left = 0
layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
group_attribs = {inkex.addNS('label', 'inkscape'): "Palette ({harmony}, {color}) ".format(color=self.color, harmony=self.options.harmony)}
palette_group = Group(**group_attribs)
for color in colors:
palette_field = Rectangle(x=str(left),
y=str(top),
width=str(size),
height=str(size))
palette_field.style = {'fill': color}
palette_group.add(palette_field)
left += size
palette_group.transform.add_translate(0, self.svg.height + size)
layer.add(palette_group)
def palette_from_raster(self, color):
# TODO: implement
return []
def opposite(self, color):
colors = opposite(color)
return colors
def splitcomplementary(self, color):
colors = splitcomplementary(color, self.options.factor)
return colors
def nhues3(self, color):
colors = nHues(color, 3)
return colors
def nhues4(self, color):
colors = nHues(color, 4)
return colors
def rectangle(self, color):
colors = rectangle(color, self.options.factor)
return colors
def fivecolors(self, color):
colors = fiveColors(color, self.options.factor)
return colors
def similar_3(self, color):
colors = similar(color, 3, self.options.factor)
return colors
def similar_5(self, color):
colors = similar(color, 5, self.options.factor)
return colors
def similaropposite(self, color):
colors = similarAndOpposite(color, self.options.factor)
return colors
def create_shades(self, colors):
shades = []
shading_options = {
"cooler": cooler,
"warmer": warmer,
"saturation": saturation,
"value": value,
"chroma": chroma,
"luma": luma,
"hue": hue,
"hue_luma": hue_luma,
"luma_plus_chroma": luma_plus_chroma,
"luma_minus_chroma": luma_minus_chroma,
}
for option, function in shading_options.items():
if vars(self.options)[option] == True:
# shades are created per color,
# but we want to get one palette per shading step
shaded_colors = []
for i in range(len(colors)):
shaded_colors.append(function(colors[i], self.options.step_width))
pals = [list(a) for a in zip(*shaded_colors)]
shades += pals
return shades
def delete_existing_palettes(self):
"""Delete all palettes in the document"""
for palette in self.get_palettes_in_doc():
palette.delete()
# Save tab
# ========
def save_palette(self, palette):
# TODO: implement
# if not hasattr(self.palette, 'name'):
# if type(file_w) in [str, unicode]:
# self.palette.name = basename(file_w)
# else:
# self.palette.name='Colors'
pass
# Colorize tab
# ============
def colorize(self):
# TODO: implement
pass
# HELPER FUNCTIONS
# ----------------
def get_palettes_in_doc(self):
palettes = []
for group in self.svg.findall('.//svg:g'):
if group.get('inkscape:label').startswith('Palette ('):
palettes.append(group)
return palettes
def sort_colors(self, colors):
if self.options.sort == "by_hue":
colors.sort(key=lambda color: color.to_hsv()[0])
elif self.options.sort == "by_saturation":
colors.sort(key=lambda color: color.to_hsv()[1])
elif self.options.sort == "by_value":
colors.sort(key=lambda color: color.to_hsv()[2])
# this option looks nicer when the palette colors are similar red tones
# some of which have a hue close to 0
# and some of which have a hue close to 1
elif self.options.sort == "hue_contiguous":
# sort by hue first
colors.sort(key=lambda color: color.to_hsv()[0])
# now find out if the hues are maybe clustered around the 0 - 1 boundary
hues = [color.to_hsv()[0] for color in colors]
start_hue = 0
end_hue = 0
max_dist = 0
for i in range(len(colors)-1):
h1 = hues[i]
h2 = hues[i+1]
cur_dist = h2-h1
if cur_dist > max_dist and self.no_colors_in_between(h1, h2, hues):
max_dist = cur_dist
start_hue = h2
for i in range(len(colors)):
sorting_hue = hues[i] - start_hue
if sorting_hue > 1:
sorting_hue -=1
elif sorting_hue < 0:
sorting_hue += 1
hues[i] = sorting_hue
sorted_colors = [color for hue, color in sorted(zip(hues,colors))]
colors = sorted_colors
else:
raise inkex.AbortExtension(
"Please select one of the following sorting options: by_hue, by_saturation, by_value.")
return colors
def no_colors_in_between(self, hue1, hue2, hues):
for hue in hues:
if hue > hue1 and hue < hue2:
return False
return True
if __name__ == '__main__':
ColorHarmony().run()

View File

@ -0,0 +1 @@
# empty

View File

@ -0,0 +1,260 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (original 'palette-editor' tool, version 0.0.7)
# 2020 Maren Hachmann (extension-ification)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from math import cos, acos, sqrt, pi
import colorsys
from inkex.colors import Color
_HCY_RED_LUMA = 0.299
_HCY_GREEN_LUMA = 0.587
_HCY_BLUE_LUMA = 0.114
class ColorPlus(Color):
#HCYwts = 0.299, 0.587, 0.114
## HCY colour space.
#
# Copy&Paste from https://raw.githubusercontent.com/mypaint/mypaint/master/gui/colors/uicolor.py
# Copyright (C) 2012-2013 by Andrew Chadwick <andrewc-git@piffle.org>
#
# Frequently referred to as HSY, Hue/Chroma/Luma, HsY, HSI etc. It can be
# thought of as a cylindrical remapping of the YCbCr solid: the "C" term is the
# proportion of the maximum permissible chroma within the RGB gamut at a given
# hue and luma. Planes of constant Y are equiluminant.
#
# ref https://code.google.com/p/colour-space-viewer/
# ref git://anongit.kde.org/kdelibs in kdeui/colors/kcolorspaces.cpp
# ref http://blog.publicfields.net/2011/12/rgb-hue-saturation-luma.html
# ref Joblove G.H., Greenberg D., Color spaces for computer graphics.
# ref http://www.cs.rit.edu/~ncs/color/t_convert.html
# ref http://en.literateprograms.org/RGB_to_HSV_color_space_conversion_(C)
# ref http://lodev.org/cgtutor/color.html
# ref Levkowitz H., Herman G.T., "GLHS: a generalized lightness, hue, and
# saturation color model"
# For consistency, use the same weights that the Color and Luminosity layer
# blend modes use, as also used by brushlib's Colorize brush blend mode. We
# follow http://www.w3.org/TR/compositing/ here. BT.601 YCbCr has a nearly
# identical definition of luma.
def __init__(self, color=None, space='rgb'):
super().__init__(color)
def to_hcy(self):
"""RGB → HCY: R,G,B,H,C,Y ∈ [0, 1]
:param rgb: Color expressed as an additive RGB triple.
:type rgb: tuple (r, g, b) where 0r1, 0g1, 0b1.
:rtype: tuple (h, c, y) where 0h<1, but 0c2 and 0y1.
"""
r, g, b = self.to_floats()
# Luma is just a weighted sum of the three components.
y = _HCY_RED_LUMA*r + _HCY_GREEN_LUMA*g + _HCY_BLUE_LUMA*b
# Hue. First pick a sector based on the greatest RGB component, then add
# the scaled difference of the other two RGB components.
p = max(r, g, b)
n = min(r, g, b)
d = p - n # An absolute measure of chroma: only used for scaling.
if n == p:
h = 0.0
elif p == r:
h = (g - b)/d
if h < 0:
h += 6.0
elif p == g:
h = ((b - r)/d) + 2.0
else: # p==b
h = ((r - g)/d) + 4.0
h /= 6.0
# Chroma, relative to the RGB gamut envelope.
if r == g == b:
# Avoid a division by zero for the achromatic case.
c = 0.0
else:
# For the derivation, see the GLHS paper.
c = max((y-n)/y, (p-y)/(1-y))
return h, c, y
@staticmethod
def from_hcy(h, c, y):
"""HCY → RGB: R,G,B,H,C,Y ∈ [0, 1]
:param hcy: Color expressed as a Hue/relative-Chroma/Luma triple.
:type hcy: tuple (h, c, y) where 0h<1, but 0c2 and 0y1.
:rtype: ColorPlus object.
>>> n = 32
>>> diffs = [sum( [abs(c1-c2) for c1, c2 in
... zip( HCY_to_RGB(RGB_to_HCY([r/n, g/n, b/n])),
... [r/n, g/n, b/n] ) ] )
... for r in range(int(n+1))
... for g in range(int(n+1))
... for b in range(int(n+1))]
>>> sum(diffs) < n*1e-6
True
"""
if c == 0:
return y, y, y
h %= 1.0
h *= 6.0
if h < 1:
#implies (p==r and h==(g-b)/d and g>=b)
th = h
tm = _HCY_RED_LUMA + _HCY_GREEN_LUMA * th
elif h < 2:
#implies (p==g and h==((b-r)/d)+2.0 and b<r)
th = 2.0 - h
tm = _HCY_GREEN_LUMA + _HCY_RED_LUMA * th
elif h < 3:
#implies (p==g and h==((b-r)/d)+2.0 and b>=g)
th = h - 2.0
tm = _HCY_GREEN_LUMA + _HCY_BLUE_LUMA * th
elif h < 4:
#implies (p==b and h==((r-g)/d)+4.0 and r<g)
th = 4.0 - h
tm = _HCY_BLUE_LUMA + _HCY_GREEN_LUMA * th
elif h < 5:
#implies (p==b and h==((r-g)/d)+4.0 and r>=g)
th = h - 4.0
tm = _HCY_BLUE_LUMA + _HCY_RED_LUMA * th
else:
#implies (p==r and h==(g-b)/d and g<b)
th = 6.0 - h
tm = _HCY_RED_LUMA + _HCY_BLUE_LUMA * th
# Calculate the RGB components in sorted order
if tm >= y:
p = y + y*c*(1-tm)/tm
o = y + y*c*(th-tm)/tm
n = y - (y*c)
else:
p = y + (1-y)*c
o = y + (1-y)*c*(th-tm)/(1-tm)
n = y - (1-y)*c*tm/(1-tm)
# Back to RGB order
if h < 1: r, g, b = p, o, n
elif h < 2: r, g, b = o, p, n
elif h < 3: r, g, b = n, p, o
elif h < 4: r, g, b = n, o, p
elif h < 5: r, g, b = o, n, p
else: r, g, b = p, n, o
return ColorPlus([255*r, 255*g, 255*b])
def to_hsv(self):
r, g, b = self.to_floats()
eps = 0.001
if abs(max(r,g,b)) < eps:
return (0,0,0)
return colorsys.rgb_to_hsv(r, g, b)
@staticmethod
def from_hsv(h, s, v):
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return ColorPlus([255*r, 255*g, 255*b])
# TODO: everything below is not updated yet, maybe not really needed
def hex(self):
r,g,b = self.getRGB()
return "#{:02x}{:02x}{:02x}".format(r,g,b)
def getRgbString(self):
r,g,b = self.getRGB()
return "rgb({}, {}, {})".format(r,g,b)
def getHsvString(self):
h,s,v = self.getHSV()
return "hsv({}, {}, {})".format(h,s,v)
def invert(self):
r, g, b = self._rgb
return Color(255-r, 255-g, 255-b)
def darker(clr, q):
h,s,v = clr.getHSV()
v = clip(v-q)
return hsv(h,s,v)
def lighter(clr, q):
h,s,v = clr.getHSV()
v = clip(v+q)
return hsv(h,s,v)
def saturate(clr, q):
h,s,v = clr.getHSV()
s = clip(s+q)
return hsv(h,s,v)
def desaturate(clr, q):
h,s,v = clr.getHSV()
s = clip(s-q)
return hsv(h,s,v)
def increment_hue(clr, q):
h,s,v = clr.getHSV()
h += q
if h > 1.0:
h -= 1.0
if h < 1.0:
h += 1.0
return hsv(h,s,v)
def contrast(clr, q):
h,s,v = clr.getHSV()
v = (v - 0.5)*(1.0 + q) + 0.5
v = clip(v)
return hsv(h,s,v)
def linear(x, y, q):
return (1.-q)*x + q*y
def linear3(v1, v2, q):
x1, y1, z1 = v1
x2, y2, z2 = v2
return (linear(x1, x2, q), linear(y1, y2, q), linear(z1, z2, q))
def circular(h1, h2, q, circle=1.0):
#print("Src hues: "+ str((h1, h2)))
d = h2 - h1
if h1 > h2:
h1, h2 = h2, h1
d = -d
q = 1.0 - q
if d > circle/2.0:
h1 = h1 + circle
h = linear(h1, h2, q)
else:
h = h1 + q*d
if h >= circle:
h -= circle
#print("Hue: "+str(h))
return h

View File

@ -0,0 +1,139 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (original 'palette-editor' tool, version 0.0.7)
# 2020 Maren Hachmann (extension-ification)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from os.path import join, basename
from lxml import etree as ET
from zipfile import ZipFile, ZIP_DEFLATED
class PaletteFile(object)
def __init__(self, name, colors, filename, folder):
self.colors = colors
if name != "":
self.name = name
else:
self.name = "Palette"
self.folder = folder
def save(self, format):
FORMATS = {'gimp': [build_gpl, 'gpl'],
'scribus': [build_scribus_xml, 'xml'],
'krita': [build_kpl, 'kpl'],
'css': [build_css, 'css'],
'android_xml': [build_android_xml, 'xml'],
}
if os.path.exists(self.folder):
# save with given file name
pass
def build_gpl(self):
# TODO: fix
palette_string = (u"Name: {}\n".format(self.palette.name).encode('utf-8'))
if hasattr(self.palette, 'ncols') and self.palette.ncols:
palette_string += 'Columns: %s\n' % self.palette.ncols
for key,value in self.palette.meta.items():
if key != "Name":
palette_string += u"# {}: {}\n".format(key, value).encode('utf-8')
palette_string += '#\n'
for row in self.palette.slots:
for slot in row:
n = slot.name
r, g, b = slot.color.getRGB()
s = '%d %d %d %s\n' % (r, g, b, n)
palette_string += s
for key,value in slot.color.meta.items():
if key != "Name":
palette_string += u"# {}: {}\n".format(key, value).encode('utf-8')
return palette_string
def build_kpl(self):
# TODO: fix (and don't save it here, only build)
with ZipFile(file_w, 'w', ZIP_DEFLATED) as zf:
zf.writestr("mimetype", MIMETYPE)
xml = ET.Element("Colorset")
xml.attrib['version'] = '1.0'
xml.attrib['columns'] = str(self.palette.ncols)
xml.attrib['name'] = self.palette.name
xml.attrib['comment'] = self.palette.meta.get("Comment", "Generated by Palette Editor")
for i,row in enumerate(self.palette.slots):
for j,slot in enumerate(row):
color = slot.color
name = color.name
default_name = "Swatch-{}-{}".format(i,j)
if not name:
name = default_name
elem = ET.SubElement(xml, "ColorSetEntry")
elem.attrib['spot'] = color.meta.get("Spot", "false")
elem.attrib['id'] = default_name
elem.attrib['name'] = name
elem.attrib['bitdepth'] = 'U8'
r,g,b = color.getRGB1()
srgb = ET.SubElement(elem, "sRGB")
srgb.attrib['r'] = str(r)
srgb.attrib['g'] = str(g)
srgb.attrib['b'] = str(b)
tree = ET.ElementTree(xml)
tree_str = ET.tostring(tree, encoding='utf-8', pretty_print=True, xml_declaration=False)
zf.writestr("colorset.xml", tree_str)
def build_scribus_xml(self):
# TODO: fix, and don't save here
xml = ET.Element("SCRIBUSCOLORS", NAME=name)
for i,row in enumerate(self.palette.getColors()):
for j,color in enumerate(row):
name = color.name
if not name:
name = "Swatch-{}-{}".format(i,j)
elem = ET.SubElement(xml, "COLOR", NAME=name, RGB=color.hex())
if "Spot" in color.meta:
elem.attrib["Spot"] = color.meta["Spot"]
if "Register" in color.meta:
elem.attrib["Register"] = color.meta["Register"]
ET.ElementTree(xml).write(file_w, encoding="utf-8", pretty_print=True, xml_declaration=True)
def build_android_xml(self):
# https://stackoverflow.com/questions/3769762/web-colors-in-an-android-color-xml-resource-file
palette_string = ''
# TODO: implement
return palette_string
def build_css(self):
palette_string = ''
# TODO: fix
for i, row in enumerate(self.palette.slots):
for j, slot in enumerate(row):
hex = slot.color.hex()
s = ".color-{}-{}{} {{ color: {} }};\n".format(i,j, user, hex)
palette_string += s
return palette_string

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
# coding=utf-8
#
# Copyright (C) 2009-2018 Ilya Portnov <portnov84@rambler.ru>
# (original 'palette-editor' tool, version 0.0.7)
# 2020 Maren Hachmann (extension-ification)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from math import sqrt, sin, cos
from color_harmony.colorplus import ColorPlus
from color_harmony.utils import seq, circle_hue
# Each of these functions takes one ColorPlus object
# and returns a list of ColorPlus objects:
__all__ = ['opposite', 'splitcomplementary', 'similar', 'similarAndOpposite', 'rectangle', 'nHues', 'fiveColors']
# Harmony functions
# 180° rotation
def opposite(color):
h, s, v = color.to_hsv()
h = h + 0.5
if h > 1.0:
h -= 1.0
return [color, ColorPlus.from_hsv(h, s, v)]
# default value 0.5 corresponds to +-36° deviation from opposite, max. useful value: 89°, min. useful value: 1°
def splitcomplementary(color, parameter=0.5):
h, s, v = color.to_hsv()
h += (1.0 - 0.4*parameter)/2.0
if h > 1.0:
h -= 1.0
c1 = ColorPlus.from_hsv(h,s,v)
h += 0.4*parameter
if h > 1.0:
h -= 1.0
c2 = ColorPlus.from_hsv(h,s,v)
return [color, c1, c2]
# default value 0.5 corresponds to 36° per step, max. useful value: 360°/n , min. useful value: 1°
def similar(color, n, parameter=0.5):
h, s, v = color.to_hsv()
step = 0.2 * parameter
hmin = h - (n // 2) * step
hmax = h + (n // 2) * step
return [ColorPlus.from_hsv(dh % 1.0, s, v) for dh in seq(hmin, hmax, step)]
# default value 0.5 corresponds to 36° deviation from original, max. useful value: 178°, min. useful value: 1°
def similarAndOpposite(color, parameter=0.5):
h, c, y = color.to_hcy()
h1 = h + 0.2 * parameter
if h1 > 1.0:
h1 -= 1.0
h2 = h - 0.2 * parameter
if h2 < 0.0:
h2 += 1.0
h3 = h + 0.5
if h3 > 1.0:
h3 -= 1.0
return [ColorPlus.from_hcy(h1,c,y),
color,
ColorPlus.from_hcy(h2,c,y),
ColorPlus.from_hcy(h3,c,y)]
# default value 0.5 corresponds to 36° deviation from original, max. useful angle 180°, min. useful angle 1°
def rectangle(color, parameter=0.5):
h, c, y = color.to_hcy()
h1 = (h + 0.2 * parameter) % 1.0
h2 = (h1 + 0.5) % 1.0
h3 = (h + 0.5) % 1.0
return [color,
ColorPlus.from_hcy(h1,c,y),
ColorPlus.from_hcy(h2,c,y),
ColorPlus.from_hcy(h3,c,y)]
# returns n colors that are placed on the hue circle in steps of 360°/n
def nHues(color, n):
h, s, v = color.to_hsv()
return [color] + [ColorPlus.from_hsv(circle_hue(i, n, h), s, v) for i in range(1, n)]
# parameter determines +/- deviation from a the hues +/-120° away, default value 0.5 corresponds to 2.16°, max. possible angle 4.32°
def fiveColors(color, parameter=0.5):
h0, s, v = color.to_hsv()
h1s = (h0 + 1.0/3.0) % 1.0
h2s = (h1s + 1.0/3.0) % 1.0
delta = 0.06 * parameter
h1 = (h1s - delta) % 1.0
h2 = (h1s + delta) % 1.0
h3 = (h2s - delta) % 1.0
h4 = (h2s + delta) % 1.0
return [color] + [ColorPlus.from_hsv(h,s,v) for h in [h1,h2,h3,h4]]

View File

@ -0,0 +1,117 @@
from color.colors import *
from color.spaces import *
def find_min(idx, occupied, d):
best_i = None
best_y = None
best_clr = None
for i, clr in d.iteritems():
if i in occupied:
continue
y = clr[idx]
if best_y is None or y < best_y:
best_i = i
best_y = y
best_clr = clr
if best_y is not None:
return best_i
for i, clr in d.iteritems():
y = clr[idx]
if best_y is None or y < best_y:
best_i = i
best_y = y
best_clr = clr
assert best_i is not None
return best_i
def find_max(idx, occupied, d):
best_i = None
best_y = None
best_clr = None
for i, clr in d.iteritems():
if i in occupied:
continue
y = clr[idx]
if best_y is None or y > best_y:
best_i = i
best_y = y
best_clr = clr
if best_y is not None:
return best_i
for i, clr in d.iteritems():
y = clr[idx]
if best_y is None or y > best_y:
best_i = i
best_y = y
best_clr = clr
assert best_i is not None
return best_i
def match_colors(colors1, colors2):
hsvs1 = dict(enumerate([c.getHCY() for c in colors1 if c is not None]))
hsvs2 = dict(enumerate([c.getHCY() for c in colors2 if c is not None]))
occupied = []
result = {}
while len(hsvs1.keys()) > 0:
# Darkest of SVG colors
darkest1_i = find_min(2, [], hsvs1)
# Darkest of palette colors
darkest2_i = find_min(2, occupied, hsvs2)
hsvs1.pop(darkest1_i)
occupied.append(darkest2_i)
result[darkest1_i] = darkest2_i
if not hsvs1:
break
# Lightest of SVG colors
lightest1_i = find_max(2, [], hsvs1)
# Lightest of palette colors
lightest2_i = find_max(2, occupied, hsvs2)
hsvs1.pop(lightest1_i)
occupied.append(lightest2_i)
result[lightest1_i] = lightest2_i
if not hsvs1:
break
# Less saturated of SVG colors
grayest1_i = find_min(1, [], hsvs1)
# Less saturated of palette colors
grayest2_i = find_min(1, occupied, hsvs2)
hsvs1.pop(grayest1_i)
occupied.append(grayest2_i)
result[grayest1_i] = grayest2_i
if not hsvs1:
break
# Most saturated of SVG colors
saturated1_i = find_max(1, [], hsvs1)
# Most saturated of palette colors
saturated2_i = find_max(1, occupied, hsvs2)
hsvs1.pop(saturated1_i)
occupied.append(saturated2_i)
result[saturated1_i] = saturated2_i
if not hsvs1:
break
redest1_i = find_min(0, [], hsvs1)
redest2_i = find_min(0, occupied, hsvs2)
hsvs1.pop(redest1_i)
occupied.append(redest2_i)
result[redest1_i] = redest2_i
if not hsvs1:
break
bluest1_i = find_max(0, [], hsvs1)
bluest2_i = find_max(0, occupied, hsvs2)
hsvs1.pop(bluest1_i)
occupied.append(bluest2_i)
result[bluest1_i] = bluest2_i
clrs = []
for i in range(len(result.keys())):
j = result[i]
clrs.append(colors2[j])
return clrs

View File

@ -0,0 +1,96 @@
import re
from lxml import etree
from color import colors
SVG_NS="http://www.w3.org/2000/svg"
color_re = re.compile("#[0-9a-fA-F]+")
def walk(processor, element):
for child in element.iter():
processor(child)
class Collector(object):
def __init__(self):
self.colors = {}
self.n = 0
def _parse(self, string):
xs = string.split(";")
single = len(xs) == 1
result = {}
for x in xs:
ts = x.split(":")
if len(ts) < 2:
if single:
return None
else:
continue
key, value = ts[0], ts[1]
result[key] = value
return result
def _merge(self, attr):
if type(attr) == str:
return attr
result = ""
for key in attr:
value = attr[key]
result += key + ":" + value + ";"
return result
def _is_color(self, val):
return color_re.match(val) is not None
def _remember_color(self, color):
if color not in self.colors:
self.colors[color] = self.n
self.n += 1
n = self.colors[color]
return "${color%s}" % n
def _process_attr(self, value):
d = self._parse(value)
if d is None:
if self._is_color(value):
return self._remember_color(value)
else:
return value
elif type(d) == dict:
for attr in ['fill', 'stroke', 'stop-color']:
if (attr in d) and self._is_color(d[attr]):
color = d[attr]
d[attr] = self._remember_color(color)
return self._merge(d)
else:
if self._is_color(value):
return self._remember_color(value)
else:
return value
def process(self, element):
for attr in ['fill', 'stroke', 'style', 'pagecolor', 'bordercolor']:
if attr in element.attrib:
value = element.get(attr)
element.set(attr, self._process_attr(value))
def result(self):
return self.colors
def read_template(filename):
xml = etree.parse(filename)
collector = Collector()
walk(collector.process, xml.getroot())
svg = etree.tostring(xml, encoding='utf-8', xml_declaration=True, pretty_print=True)
#open("last_template.svg",'w').write(svg)
color_dict = collector.result()
colors_inv = dict((v,k) for k, v in color_dict.iteritems())
svg_colors = []
for key in range(len(colors_inv.keys())):
clr = colors_inv[key]
svg_colors.append( colors.fromHex(clr) )
return svg_colors, svg

View File

@ -0,0 +1,135 @@
from string import Template
from PyQt4 import QtGui, QtSvg, QtCore
from color.colors import *
from color.spaces import *
import svg, transform, matching
class SvgTemplateWidget(QtSvg.QSvgWidget):
template_loaded = QtCore.pyqtSignal()
colors_matched = QtCore.pyqtSignal()
file_dropped = QtCore.pyqtSignal(unicode)
def __init__(self, *args):
QtSvg.QSvgWidget.__init__(self, *args)
self.setAcceptDrops(True)
self._colors = [Color(i*10,i*10,i*10) for i in range(20)]
self._template = None
self._template_filename = None
self._svg = None
self._need_render = True
self._svg_colors = None
self._dst_colors = None
self._last_size = None
def sizeHint(self):
if self.renderer().isValid():
return self.renderer().defaultSize()
elif self._last_size:
return self._last_size
else:
return QtCore.QSize(300,300)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event):
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
path = unicode( urls[0].path() )
self.file_dropped.emit(path)
def _get_color(self, i):
if i < len(self._colors):
return self._colors[i]
else:
return Color(i*10, i*10, i*10)
def _update(self):
arr = QtCore.QByteArray.fromRawData(self.get_svg())
print("Data loaded: {} bytes".format(arr.length()))
self.load(arr)
if self.renderer().isValid():
self._last_size = self.renderer().defaultSize()
self.update()
def _get_image(self):
w,h = self.size().width(), self.size().height()
image = QtGui.QImage(w, h, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(0)
qp = QtGui.QPainter()
qp.begin(image)
self.renderer().render(qp, QtCore.QRectF(0.0, 0.0, w, h))
qp.end()
return image
def loadTemplate(self, filename):
self._template_filename = filename
self._svg_colors, self._template = svg.read_template(filename)
print("Source SVG colors:")
for c in self._svg_colors:
print str(c)
print("Template loaded: {}: {} bytes".format(filename, len(self._template)))
self._need_render = True
self._update()
self.template_loaded.emit()
def set_color(self, idx, color):
self._colors[idx] = color
self._need_render = True
self._update()
def setColors(self, dst_colors, space=HCY):
if not dst_colors:
return
print("Matching colors in space: {}".format(space))
self._dst_colors = dst_colors
self._colors = transform.match_colors(space, self._svg_colors, dst_colors)
#self._colors = matching.match_colors(self._svg_colors, dst_colors)
self._need_render = True
self._update()
self.colors_matched.emit()
def resetColors(self):
self.load(self._template_filename)
self.repaint()
def get_svg_colors(self):
return self._svg_colors
def get_dst_colors(self):
return self._colors
def get_svg(self):
if self._svg is not None and not self._need_render:
return self._svg
else:
self._svg = self._render()
self._need_render = False
return self._svg
def _render(self):
#d = dict([("color"+str(i), color.hex() if color is not None else Color(255,255,255)) for i, color in enumerate(self._colors)])
d = ColorDict(self._colors)
#self._image = self._get_image()
return Template(self._template).substitute(d)
class ColorDict(object):
def __init__(self, colors):
self._colors = colors
def __getitem__(self, key):
if key.startswith("color"):
n = int( key[5:] )
if n < len(self._colors):
return self._colors[n].hex()
else:
return "#ffffff"
else:
raise KeyError(key)

View File

@ -0,0 +1,243 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# -y11+a13*x13+a12*x12+a11*x11+b1
# -y12+a23*x13+a22*x12+a21*x11+b2
# -y13+a33*x13+a32*x12+a31*x11+b3
# -y21+a13*x23+a12*x22+a11*x21+b1
# -y22+a23*x23+a22*x22+a21*x21+b2
# -y23+a33*x23+a32*x22+a31*x21+b3
# -y31+a13*x33+a12*x32+a11*x31+b1
# -y32+a23*x33+a22*x32+a21*x31+b2
# -y33+a33*x33+a32*x32+a31*x31+b3
# -y41+a13*x43+a12*x42+a11*x41+b1
# -y42+a23*x43+a22*x42+a21*x41+b2
# -y43+a33*x43+a32*x42+a31*x41+b3
from math import sqrt
import itertools
import numpy as np
from numpy.linalg import solve, det
from numpy.linalg.linalg import LinAlgError
from copy import deepcopy as copy
from PyQt4 import QtGui
from color.colors import *
from color.spaces import *
def get_A(x):
return np.array([[x[0][0], x[0][1], x[0][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, x[0][0], x[0][1], x[0][2], 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, x[0][0], x[0][1], x[0][2], 0, 0, 1],
[x[1][0], x[1][1], x[1][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, x[1][0], x[1][1], x[1][2], 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, x[1][0], x[1][1], x[1][2], 0, 0, 1],
[x[2][0], x[2][1], x[2][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, x[2][0], x[2][1], x[2][2], 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, x[2][0], x[2][1], x[2][2], 0, 0, 1],
[x[3][0], x[3][1], x[3][2], 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, x[3][0], x[3][1], x[3][2], 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, x[3][0], x[3][1], x[3][2], 0, 0, 1] ])
def get_B(y):
return np.array([[y[0][0]], [y[0][1]], [y[0][2]],
[y[1][0]], [y[1][1]], [y[1][2]],
[y[2][0]], [y[2][1]], [y[2][2]],
[y[3][0]], [y[3][1]], [y[3][2]] ])
def color_row(space, color):
x1,x2,x3 = space.getCoords(color)
return np.array([x1,x2,x3])
def color_column(space, color):
x1,x2,x3 = space.getCoords(color)
return np.array([[x1],[x2],[x3]])
def colors_array(space, *colors):
return np.array([color_row(space, c) for c in colors])
def find_transform_colors(space, cx1, cx2, cx3, cx4, cy1, cy2, cy3, cy4):
x = colors_array(space, cx1, cx2, cx3, cx4)
y = colors_array(space, cy1, cy2, cy3, cy4)
return find_transform(x,y)
def find_transform(x, y):
#print("X:\n"+str(x))
#print("Y:\n"+str(y))
m = solve(get_A(x), get_B(y))
a = np.array([[m[0][0], m[1][0], m[2][0]],
[m[3][0], m[4][0], m[5][0]],
[m[6][0], m[7][0], m[8][0]]])
b = np.array([[m[9][0]], [m[10][0]], [m[11][0]]])
return (a, b)
def transform_colors(space, a, b, cx):
x = color_column(space, cx)
y = a.dot(x) + b
#print("X: " + str(x))
#print("Y: " + str(y))
return space.fromCoords(y[0][0], y[1][0], y[2][0])
def transform(a, b, x):
#print(type(x))
return a.dot(x[:,None]) + b
def rhoC(space, color1, color2):
x1,y1,z1 = space.getCoords(color1)
x2,y2,z2 = space.getCoords(color2)
return sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
def rho(c1, c2):
x1,y1,z1 = c1[0], c1[1], c1[2]
x2,y2,z2 = c2[0], c2[1], c2[2]
return sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
def get_center(points):
zero = np.array([0,0,0])
return sum(points, zero) / float(len(points))
def get_center_color(space, colors):
zero = np.array([0,0,0])
points = [space.getCoords(c) for c in colors]
center = sum(points, zero) / float(len(points))
c1,c2,c3 = center[0], center[1], center[2]
return space.fromCoords((c1,c2,c3))
def get_nearest(x, occupied, points):
min_rho = None
min_p = None
for p in points:
if any([(p == o).all() for o in occupied]):
continue
r = rho(x,p)
if min_rho is None or r < min_rho:
min_rho = r
min_p = p
if min_p is not None:
occupied.append(min_p)
#print("Now occupied : " + str(x))
return min_p
#print("Was occupied : " + str(x))
for p in points:
r = rho(x,p)
if min_rho is None or r < min_rho:
min_rho = r
min_p = p
return min_p
def get_nearest_color(space, occupied, cx, colors):
points = [space.getCoords(c) for c in colors]
cy = get_nearest(space.getCoords(cx), occupied, points)
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])

View File

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

View File

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

View 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)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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