plug-ins: Port palette-sort to Python 3
This is a basic port without any UI. Invoking the plugin will just sort the entire palette based on default parameters. The original plugin had several broken options, which I tried to fix.
This commit is contained in:
@ -1,356 +0,0 @@
|
|||||||
#!/usr/bin/env python2
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# 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 3 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, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from gimpfu import *
|
|
||||||
# little known, colorsys is part of Python's stdlib
|
|
||||||
from colorsys import rgb_to_yiq
|
|
||||||
from textwrap import dedent
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
|
|
||||||
|
|
||||||
AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
|
|
||||||
_("Luma (Y)"),
|
|
||||||
_("Hue"), _("Saturation"), _("Value"),
|
|
||||||
_("Saturation (HSL)"), _("Lightness (HSL)"),
|
|
||||||
_("Index"),
|
|
||||||
_("Random"))
|
|
||||||
|
|
||||||
GRAIN_SCALE = (1.0, 1.0 , 1.0,
|
|
||||||
1.0,
|
|
||||||
360., 100., 100.,
|
|
||||||
100., 100.,
|
|
||||||
16384.,
|
|
||||||
float(0x7ffffff),
|
|
||||||
100., 256., 256.,
|
|
||||||
256., 360.,)
|
|
||||||
|
|
||||||
SELECT_ALL = 0
|
|
||||||
SELECT_SLICE = 1
|
|
||||||
SELECT_AUTOSLICE = 2
|
|
||||||
SELECT_PARTITIONED = 3
|
|
||||||
SELECTIONS = (SELECT_ALL, SELECT_SLICE, SELECT_AUTOSLICE, SELECT_PARTITIONED)
|
|
||||||
|
|
||||||
|
|
||||||
def noop(v, i):
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def to_hsv(v, i):
|
|
||||||
return v.to_hsv()
|
|
||||||
|
|
||||||
|
|
||||||
def to_hsl(v, i):
|
|
||||||
return v.to_hsl()
|
|
||||||
|
|
||||||
|
|
||||||
def to_yiq(v, i):
|
|
||||||
return rgb_to_yiq(*v[:-1])
|
|
||||||
|
|
||||||
|
|
||||||
def to_index(v, i):
|
|
||||||
return (i,)
|
|
||||||
|
|
||||||
def to_random(v, i):
|
|
||||||
return (randint(0, 0x7fffffff),)
|
|
||||||
|
|
||||||
|
|
||||||
channel_getters = [ (noop, 0), (noop, 1), (noop, 2),
|
|
||||||
(to_yiq, 0),
|
|
||||||
(to_hsv, 0), (to_hsv, 1), (to_hsv, 2),
|
|
||||||
(to_hsl, 1), (to_hsl, 2),
|
|
||||||
(to_index, 0),
|
|
||||||
(to_random, 0)]
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from colormath.color_objects import RGBColor, LabColor, LCHabColor
|
|
||||||
AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
|
|
||||||
_("A-color"), _("B-color"),
|
|
||||||
_("Chroma (LCHab)"),
|
|
||||||
_("Hue (LCHab)"))
|
|
||||||
to_lab = lambda v,i: RGBColor(*v[:-1]).convert_to('LAB').get_value_tuple()
|
|
||||||
to_lchab = (lambda v,i:
|
|
||||||
RGBColor(*v[:-1]).convert_to('LCHab').get_value_tuple())
|
|
||||||
channel_getters.extend([(to_lab, 0), (to_lab, 1), (to_lab, 2),
|
|
||||||
(to_lchab, 1), (to_lchab, 2)])
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_slice(s, numcolors):
|
|
||||||
"""Parse a slice spec and return (start, nrows, length)
|
|
||||||
All items are optional. Omitting them makes the largest possible selection that
|
|
||||||
exactly fits the other items.
|
|
||||||
|
|
||||||
start:nrows,length
|
|
||||||
|
|
||||||
|
|
||||||
'' selects all items, as does ':'
|
|
||||||
':4,' makes a 4-row selection out of all colors (length auto-determined)
|
|
||||||
':4' also.
|
|
||||||
':1,4' selects the first 4 colors
|
|
||||||
':,4' selects rows of 4 colors (nrows auto-determined)
|
|
||||||
':4,4' selects 4 rows of 4 colors
|
|
||||||
'4:' selects a single row of all colors after 4, inclusive.
|
|
||||||
'4:,4' selects rows of 4 colors, starting at 4 (nrows auto-determined)
|
|
||||||
'4:4,4' selects 4 rows of 4 colors (16 colors total), beginning at index 4.
|
|
||||||
'4' is illegal (ambiguous)
|
|
||||||
|
|
||||||
|
|
||||||
In general, slices are comparable to a numpy sub-array.
|
|
||||||
'start at element START, with shape (NROWS, LENGTH)'
|
|
||||||
|
|
||||||
"""
|
|
||||||
s = s.strip()
|
|
||||||
|
|
||||||
def notunderstood():
|
|
||||||
raise ValueError('Slice %r not understood. Should be in format'
|
|
||||||
' START?:NROWS?,ROWLENGTH? eg. "0:4,16".' % s)
|
|
||||||
def _int(v):
|
|
||||||
try:
|
|
||||||
return int(v)
|
|
||||||
except ValueError:
|
|
||||||
notunderstood()
|
|
||||||
if s in ('', ':', ':,'):
|
|
||||||
return 0, 1, numcolors # entire palette, one row
|
|
||||||
if s.count(':') != 1:
|
|
||||||
notunderstood()
|
|
||||||
rowpos = s.find(':')
|
|
||||||
start = 0
|
|
||||||
if rowpos > 0:
|
|
||||||
start = _int(s[:rowpos])
|
|
||||||
numcolors -= start
|
|
||||||
nrows = 1
|
|
||||||
if ',' in s:
|
|
||||||
commapos = s.find(',')
|
|
||||||
nrows = s[rowpos+1:commapos]
|
|
||||||
length = s[commapos+1:]
|
|
||||||
if not nrows:
|
|
||||||
if not length:
|
|
||||||
notunderstood()
|
|
||||||
else:
|
|
||||||
length = _int(length)
|
|
||||||
if length == 0:
|
|
||||||
notunderstood()
|
|
||||||
nrows = numcolors // length
|
|
||||||
if numcolors % length:
|
|
||||||
nrows = -nrows
|
|
||||||
elif not length:
|
|
||||||
nrows = _int(nrows)
|
|
||||||
if nrows == 0:
|
|
||||||
notunderstood()
|
|
||||||
length = numcolors // nrows
|
|
||||||
if numcolors % nrows:
|
|
||||||
length = -length
|
|
||||||
else:
|
|
||||||
nrows = _int(nrows)
|
|
||||||
if nrows == 0:
|
|
||||||
notunderstood()
|
|
||||||
length = _int(length)
|
|
||||||
if length == 0:
|
|
||||||
notunderstood()
|
|
||||||
else:
|
|
||||||
nrows = _int(s[rowpos+1:])
|
|
||||||
if nrows == 0:
|
|
||||||
notunderstood()
|
|
||||||
length = numcolors // nrows
|
|
||||||
if numcolors % nrows:
|
|
||||||
length = -length
|
|
||||||
return start, nrows, length
|
|
||||||
|
|
||||||
|
|
||||||
def quantization_grain(channel, g):
|
|
||||||
"Given a channel and a quantization, return the size of a quantization grain"
|
|
||||||
g = max(1.0, g)
|
|
||||||
if g <= 1.0:
|
|
||||||
g = 0.00001
|
|
||||||
else:
|
|
||||||
g = max(0.00001, GRAIN_SCALE[channel] / g)
|
|
||||||
return g
|
|
||||||
|
|
||||||
|
|
||||||
def palette_sort(palette, selection, slice_expr, channel1, ascending1,
|
|
||||||
channel2, ascending2, quantize, pchannel, pquantize):
|
|
||||||
|
|
||||||
grain1 = quantization_grain(channel1, quantize)
|
|
||||||
grain2 = quantization_grain(channel2, quantize)
|
|
||||||
pgrain = quantization_grain(pchannel, pquantize)
|
|
||||||
|
|
||||||
#If palette is read only, work on a copy:
|
|
||||||
editable = pdb.gimp_palette_is_editable(palette)
|
|
||||||
if not editable:
|
|
||||||
palette = pdb.gimp_palette_duplicate (palette)
|
|
||||||
|
|
||||||
num_colors = pdb.gimp_palette_get_info (palette)
|
|
||||||
|
|
||||||
start, nrows, length = None, None, None
|
|
||||||
if selection == SELECT_AUTOSLICE:
|
|
||||||
def find_index(color, startindex=0):
|
|
||||||
for i in range(startindex, num_colors):
|
|
||||||
c = pdb.gimp_palette_entry_get_color (palette, i)
|
|
||||||
if c == color:
|
|
||||||
return i
|
|
||||||
return None
|
|
||||||
def hexcolor(c):
|
|
||||||
return "#%02x%02x%02x" % tuple(c[:-1])
|
|
||||||
fg = pdb.gimp_context_get_foreground()
|
|
||||||
bg = pdb.gimp_context_get_background()
|
|
||||||
start = find_index(fg)
|
|
||||||
end = find_index(bg)
|
|
||||||
if start is None:
|
|
||||||
raise ValueError("Couldn't find foreground color %r in palette" % list(fg))
|
|
||||||
if end is None:
|
|
||||||
raise ValueError("Couldn't find background color %r in palette" % list(bg))
|
|
||||||
if find_index(fg, start + 1):
|
|
||||||
raise ValueError('Autoslice cannot be used when more than one'
|
|
||||||
' instance of an endpoint'
|
|
||||||
' (%s) is present' % hexcolor(fg))
|
|
||||||
if find_index(bg, end + 1):
|
|
||||||
raise ValueError('Autoslice cannot be used when more than one'
|
|
||||||
' instance of an endpoint'
|
|
||||||
' (%s) is present' % hexcolor(bg))
|
|
||||||
if start > end:
|
|
||||||
end, start = start, end
|
|
||||||
length = (end - start) + 1
|
|
||||||
try:
|
|
||||||
_, nrows, _ = parse_slice(slice_expr, length)
|
|
||||||
nrows = abs(nrows)
|
|
||||||
if length % nrows:
|
|
||||||
raise ValueError('Total length %d not evenly divisible'
|
|
||||||
' by number of rows %d' % (length, nrows))
|
|
||||||
length /= nrows
|
|
||||||
except ValueError:
|
|
||||||
# bad expression is okay here, just assume one row
|
|
||||||
nrows = 1
|
|
||||||
# remaining behaviour is implemented by SELECT_SLICE 'inheritance'.
|
|
||||||
selection= SELECT_SLICE
|
|
||||||
elif selection in (SELECT_SLICE, SELECT_PARTITIONED):
|
|
||||||
start, nrows, length = parse_slice(slice_expr, num_colors)
|
|
||||||
|
|
||||||
channels_getter_1, channel_index = channel_getters[channel1]
|
|
||||||
channels_getter_2, channel2_index = channel_getters[channel2]
|
|
||||||
|
|
||||||
def get_colors(start, end):
|
|
||||||
result = []
|
|
||||||
for i in range(start, end):
|
|
||||||
entry = (pdb.gimp_palette_entry_get_name (palette, i),
|
|
||||||
pdb.gimp_palette_entry_get_color (palette, i))
|
|
||||||
index1 = channels_getter_1(entry[1], i)[channel_index]
|
|
||||||
index2 = channels_getter_2(entry[1], i)[channel2_index]
|
|
||||||
index = ((index1 - (index1 % grain1)) * (1 if ascending1 else -1),
|
|
||||||
(index2 - (index2 % grain2)) * (1 if ascending2 else -1)
|
|
||||||
)
|
|
||||||
result.append((index, entry))
|
|
||||||
return result
|
|
||||||
|
|
||||||
if selection == SELECT_ALL:
|
|
||||||
entry_list = get_colors(0, num_colors)
|
|
||||||
entry_list.sort(key=lambda v:v[0])
|
|
||||||
for i in range(num_colors):
|
|
||||||
pdb.gimp_palette_entry_set_name (palette, i, entry_list[i][1][0])
|
|
||||||
pdb.gimp_palette_entry_set_color (palette, i, entry_list[i][1][1])
|
|
||||||
|
|
||||||
elif selection == SELECT_PARTITIONED:
|
|
||||||
if num_colors < (start + length * nrows) - 1:
|
|
||||||
raise ValueError('Not enough entries in palette to '
|
|
||||||
'sort complete rows! Got %d, expected >=%d' %
|
|
||||||
(num_colors, start + length * nrows))
|
|
||||||
pchannels_getter, pchannel_index = channel_getters[pchannel]
|
|
||||||
for row in range(nrows):
|
|
||||||
partition_spans = [1]
|
|
||||||
rowstart = start + (row * length)
|
|
||||||
old_color = pdb.gimp_palette_entry_get_color (palette,
|
|
||||||
rowstart)
|
|
||||||
old_partition = pchannels_getter(old_color, rowstart)[pchannel_index]
|
|
||||||
old_partition = old_partition - (old_partition % pgrain)
|
|
||||||
for i in range(rowstart + 1, rowstart + length):
|
|
||||||
this_color = pdb.gimp_palette_entry_get_color (palette, i)
|
|
||||||
this_partition = pchannels_getter(this_color, i)[pchannel_index]
|
|
||||||
this_partition = this_partition - (this_partition % pgrain)
|
|
||||||
if this_partition == old_partition:
|
|
||||||
partition_spans[-1] += 1
|
|
||||||
else:
|
|
||||||
partition_spans.append(1)
|
|
||||||
old_partition = this_partition
|
|
||||||
base = rowstart
|
|
||||||
for size in partition_spans:
|
|
||||||
palette_sort(palette, SELECT_SLICE, '%d:1,%d' % (base, size),
|
|
||||||
channel, quantize, ascending, 0, 1.0)
|
|
||||||
base += size
|
|
||||||
else:
|
|
||||||
stride = length
|
|
||||||
if num_colors < (start + stride * nrows) - 1:
|
|
||||||
raise ValueError('Not enough entries in palette to sort '
|
|
||||||
'complete rows! Got %d, expected >=%d' %
|
|
||||||
(num_colors, start + stride * nrows))
|
|
||||||
|
|
||||||
for row_start in range(start, start + stride * nrows, stride):
|
|
||||||
sublist = get_colors(row_start, row_start + stride)
|
|
||||||
sublist.sort(key=lambda v:v[0], reverse=not ascending)
|
|
||||||
for i, entry in zip(range(row_start, row_start + stride), sublist):
|
|
||||||
pdb.gimp_palette_entry_set_name (palette, i, entry[1][0])
|
|
||||||
pdb.gimp_palette_entry_set_color (palette, i, entry[1][1])
|
|
||||||
|
|
||||||
return palette
|
|
||||||
|
|
||||||
register(
|
|
||||||
"python-fu-palette-sort",
|
|
||||||
N_("Sort the colors in a palette"),
|
|
||||||
# FIXME: Write humanly readable help -
|
|
||||||
# (I can't figure out what the plugin does, or how to use the parameters after
|
|
||||||
# David's enhancements even looking at the code -
|
|
||||||
# let alone someone just using GIMP (JS) )
|
|
||||||
dedent("""\
|
|
||||||
palette_sort (palette, selection, slice_expr, channel,
|
|
||||||
channel2, quantize, ascending, pchannel, pquantize) -> new_palette
|
|
||||||
Sorts a palette, or part of a palette, using several options.
|
|
||||||
One can select two color channels over which to sort,
|
|
||||||
and several auxiliary parameters create a 2D sorted
|
|
||||||
palette with sorted rows, among other things.
|
|
||||||
One can optionally install colormath
|
|
||||||
(https://pypi.python.org/pypi/colormath/1.0.8)
|
|
||||||
to GIMP's Python to get even more channels to choose from.
|
|
||||||
"""),
|
|
||||||
"João S. O. Bueno, Carol Spears, David Gowers",
|
|
||||||
"João S. O. Bueno, Carol Spears, David Gowers",
|
|
||||||
"2006-2014",
|
|
||||||
N_("_Sort Palette..."),
|
|
||||||
"",
|
|
||||||
[
|
|
||||||
(PF_PALETTE, "palette", _("Palette"), ""),
|
|
||||||
(PF_OPTION, "selections", _("Se_lections"), SELECT_ALL,
|
|
||||||
(_("All"), _("Slice / Array"), _("Autoslice (fg->bg)"),
|
|
||||||
_("Partitioned"))),
|
|
||||||
(PF_STRING, "slice-expr", _("Slice _expression"), ''),
|
|
||||||
(PF_OPTION, "channel1", _("Channel to _sort"), 3,
|
|
||||||
AVAILABLE_CHANNELS),
|
|
||||||
(PF_BOOL, "ascending1", _("_Ascending"), True),
|
|
||||||
(PF_OPTION, "channel2", _("Secondary Channel to s_ort"), 5,
|
|
||||||
AVAILABLE_CHANNELS),
|
|
||||||
(PF_BOOL, "ascending2", _("_Ascending"), True),
|
|
||||||
(PF_FLOAT, "quantize", _("_Quantization"), 0.0),
|
|
||||||
(PF_OPTION, "pchannel", _("_Partitioning channel"), 3,
|
|
||||||
AVAILABLE_CHANNELS),
|
|
||||||
(PF_FLOAT, "pquantize", _("Partition q_uantization"), 0.0),
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
palette_sort,
|
|
||||||
menu="<Palettes>",
|
|
||||||
domain=("gimp20-python", gimp.locale_directory)
|
|
||||||
)
|
|
||||||
|
|
||||||
main ()
|
|
@ -18,6 +18,9 @@ gradients_save_as_css_SCRIPTS = gradients-save-as-css.py
|
|||||||
palette_offsetdir = $(gimpplugindir)/plug-ins/palette-offset
|
palette_offsetdir = $(gimpplugindir)/plug-ins/palette-offset
|
||||||
palette_offset_SCRIPTS = palette-offset.py
|
palette_offset_SCRIPTS = palette-offset.py
|
||||||
|
|
||||||
|
palette_sortdir = $(gimpplugindir)/plug-ins/palette-sort
|
||||||
|
palette_sort_SCRIPTS = palette-sort.py
|
||||||
|
|
||||||
palette_to_gradientdir = $(gimpplugindir)/plug-ins/palette-to-gradient
|
palette_to_gradientdir = $(gimpplugindir)/plug-ins/palette-to-gradient
|
||||||
palette_to_gradient_SCRIPTS = palette-to-gradient.py
|
palette_to_gradient_SCRIPTS = palette-to-gradient.py
|
||||||
|
|
||||||
@ -29,7 +32,6 @@ spyro_plus_SCRIPTS = spyro-plus.py
|
|||||||
|
|
||||||
# TODO: to be ported:
|
# TODO: to be ported:
|
||||||
## histogram-export.py
|
## histogram-export.py
|
||||||
## palette-sort.py
|
|
||||||
## python-eval.py
|
## python-eval.py
|
||||||
|
|
||||||
if GIMP_UNSTABLE
|
if GIMP_UNSTABLE
|
||||||
@ -46,6 +48,7 @@ EXTRA_DIST = \
|
|||||||
foggify.py \
|
foggify.py \
|
||||||
gradients-save-as-css.py \
|
gradients-save-as-css.py \
|
||||||
palette-offset.py \
|
palette-offset.py \
|
||||||
|
palette-sort.py \
|
||||||
palette-to-gradient.py \
|
palette-to-gradient.py \
|
||||||
py-slice.py \
|
py-slice.py \
|
||||||
spyro-plus.py
|
spyro-plus.py
|
||||||
|
@ -8,12 +8,12 @@ plugins = [
|
|||||||
{ 'name': 'foggify' },
|
{ 'name': 'foggify' },
|
||||||
{ 'name': 'gradients-save-as-css' },
|
{ 'name': 'gradients-save-as-css' },
|
||||||
{ 'name': 'palette-offset' },
|
{ 'name': 'palette-offset' },
|
||||||
|
{ 'name': 'palette-sort' },
|
||||||
{ 'name': 'palette-to-gradient' },
|
{ 'name': 'palette-to-gradient' },
|
||||||
{ 'name': 'py-slice' },
|
{ 'name': 'py-slice' },
|
||||||
{ 'name': 'spyro-plus' },
|
{ 'name': 'spyro-plus' },
|
||||||
|
|
||||||
# { 'name': 'histogram-export' },
|
# { 'name': 'histogram-export' },
|
||||||
# { 'name': 'palette-sort' },
|
|
||||||
# { 'name': 'python-eval' },
|
# { 'name': 'python-eval' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
492
plug-ins/python/palette-sort.py
Executable file
492
plug-ins/python/palette-sort.py
Executable file
@ -0,0 +1,492 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# 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 3 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# For a detailed explanation of the parameters of this plugin, see :
|
||||||
|
# https://gitlab.gnome.org/GNOME/gimp/-/issues/4368#note_763460
|
||||||
|
|
||||||
|
# little known, colorsys is part of Python's stdlib
|
||||||
|
from colorsys import rgb_to_yiq
|
||||||
|
from textwrap import dedent
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gimp', '3.0')
|
||||||
|
from gi.repository import Gimp
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import Gio
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
_ = gettext.gettext
|
||||||
|
def N_(message): return message
|
||||||
|
|
||||||
|
|
||||||
|
AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
|
||||||
|
_("Luma (Y)"),
|
||||||
|
_("Hue"), _("Saturation"), _("Value"),
|
||||||
|
_("Saturation (HSL)"), _("Lightness (HSL)"),
|
||||||
|
_("Index"),
|
||||||
|
_("Random"))
|
||||||
|
|
||||||
|
channel_getters = [
|
||||||
|
(lambda v, i: v.r),
|
||||||
|
(lambda v, i: v.g),
|
||||||
|
(lambda v, i: v.r),
|
||||||
|
|
||||||
|
(lambda v, i: rgb_to_yiq(v.r, v.g, v.b)[0]),
|
||||||
|
|
||||||
|
(lambda v, i: v.to_hsv().h),
|
||||||
|
(lambda v, i: v.to_hsv().s),
|
||||||
|
(lambda v, i: v.to_hsv().v),
|
||||||
|
|
||||||
|
(lambda v, i: v.to_hsl().s),
|
||||||
|
(lambda v, i: v.to_hsl().l),
|
||||||
|
|
||||||
|
(lambda v, i: i),
|
||||||
|
(lambda v, i: randint(0, 0x7fffffff))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
GRAIN_SCALE = (1.0, 1.0 , 1.0,
|
||||||
|
1.0,
|
||||||
|
360., 100., 100.,
|
||||||
|
100., 100.,
|
||||||
|
16384.,
|
||||||
|
float(0x7ffffff),
|
||||||
|
100., 256., 256.,
|
||||||
|
256., 360.,)
|
||||||
|
|
||||||
|
SELECT_ALL = 0
|
||||||
|
SELECT_SLICE = 1
|
||||||
|
SELECT_AUTOSLICE = 2
|
||||||
|
SELECT_PARTITIONED = 3
|
||||||
|
SELECTIONS = (SELECT_ALL, SELECT_SLICE, SELECT_AUTOSLICE, SELECT_PARTITIONED)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from colormath.color_objects import RGBColor, LabColor, LCHabColor
|
||||||
|
|
||||||
|
def to_lab(v):
|
||||||
|
return RGBColor(v.r, v.g, v.b).convert_to('LAB').get_value_tuple()
|
||||||
|
|
||||||
|
def to_lchab(v):
|
||||||
|
return RGBColor(v.r, v.g, v.b).convert_to('LCHab').get_value_tuple()
|
||||||
|
|
||||||
|
AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
|
||||||
|
_("A-color"), _("B-color"),
|
||||||
|
_("Chroma (LCHab)"),
|
||||||
|
_("Hue (LCHab)"))
|
||||||
|
channel_getters.extend([
|
||||||
|
(lambda v, i: to_lab(v)[0]),
|
||||||
|
(lambda v, i: to_lab(v)[1]),
|
||||||
|
(lambda v, i: to_lab(v)[2]),
|
||||||
|
|
||||||
|
(lambda v, i: to_lchab(v)[1]),
|
||||||
|
(lambda v, i: to_lchab(v)[2])
|
||||||
|
])
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
slice_expr_doc = """
|
||||||
|
Format is 'start:nrows,length' . All items are optional.
|
||||||
|
|
||||||
|
The empty string selects all items, as does ':'
|
||||||
|
':4,' makes a 4-row selection out of all colors (length auto-determined)
|
||||||
|
':4' also.
|
||||||
|
':1,4' selects the first 4 colors
|
||||||
|
':,4' selects rows of 4 colors (nrows auto-determined)
|
||||||
|
':3,4' selects 3 rows of 4 colors
|
||||||
|
'4:' selects a single row of all colors after 4, inclusive.
|
||||||
|
'3:,4' selects rows of 4 colors, starting at 3 (nrows auto-determined)
|
||||||
|
'2:3,4' selects 3 rows of 4 colors (12 colors total), beginning at index 2.
|
||||||
|
'4' is illegal (ambiguous)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_slice(s, numcolors):
|
||||||
|
"""Parse a slice spec and return (start, nrows, length)
|
||||||
|
All items are optional. Omitting them makes the largest possible selection that
|
||||||
|
exactly fits the other items.
|
||||||
|
|
||||||
|
In general, slices are comparable to a numpy sub-array.
|
||||||
|
'start at element START, with shape (NROWS, LENGTH)'
|
||||||
|
|
||||||
|
"""
|
||||||
|
s = s.strip()
|
||||||
|
|
||||||
|
def notunderstood():
|
||||||
|
raise ValueError('Slice %r not understood. Should be in format'
|
||||||
|
' START?:NROWS?,ROWLENGTH? eg. "0:4,16".' % s)
|
||||||
|
def _int(v):
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except ValueError:
|
||||||
|
notunderstood()
|
||||||
|
if s in ('', ':', ':,'):
|
||||||
|
return 0, 1, numcolors # entire palette, one row
|
||||||
|
if s.count(':') != 1:
|
||||||
|
notunderstood()
|
||||||
|
rowpos = s.find(':')
|
||||||
|
start = 0
|
||||||
|
if rowpos > 0:
|
||||||
|
start = _int(s[:rowpos])
|
||||||
|
numcolors -= start
|
||||||
|
nrows = 1
|
||||||
|
if ',' in s:
|
||||||
|
commapos = s.find(',')
|
||||||
|
nrows = s[rowpos+1:commapos]
|
||||||
|
length = s[commapos+1:]
|
||||||
|
if not nrows:
|
||||||
|
if not length:
|
||||||
|
notunderstood()
|
||||||
|
else:
|
||||||
|
length = _int(length)
|
||||||
|
if length == 0:
|
||||||
|
notunderstood()
|
||||||
|
nrows = numcolors // length
|
||||||
|
if numcolors % length:
|
||||||
|
nrows = -nrows
|
||||||
|
elif not length:
|
||||||
|
nrows = _int(nrows)
|
||||||
|
if nrows == 0:
|
||||||
|
notunderstood()
|
||||||
|
length = numcolors // nrows
|
||||||
|
if numcolors % nrows:
|
||||||
|
length = -length
|
||||||
|
else:
|
||||||
|
nrows = _int(nrows)
|
||||||
|
if nrows == 0:
|
||||||
|
notunderstood()
|
||||||
|
length = _int(length)
|
||||||
|
if length == 0:
|
||||||
|
notunderstood()
|
||||||
|
else:
|
||||||
|
nrows = _int(s[rowpos+1:])
|
||||||
|
if nrows == 0:
|
||||||
|
notunderstood()
|
||||||
|
length = numcolors // nrows
|
||||||
|
if numcolors % nrows:
|
||||||
|
length = -length
|
||||||
|
return start, nrows, length
|
||||||
|
|
||||||
|
|
||||||
|
def quantization_grain(channel, g):
|
||||||
|
"Given a channel and a quantization, return the size of a quantization grain"
|
||||||
|
g = max(1.0, g)
|
||||||
|
if g <= 1.0:
|
||||||
|
g = 0.00001
|
||||||
|
else:
|
||||||
|
g = max(0.00001, GRAIN_SCALE[channel] / g)
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def palette_sort(palette, selection, slice_expr, channel1, ascending1,
|
||||||
|
channel2, ascending2, quantize, pchannel, pquantize):
|
||||||
|
|
||||||
|
grain1 = quantization_grain(channel1, quantize)
|
||||||
|
grain2 = quantization_grain(channel2, quantize)
|
||||||
|
pgrain = quantization_grain(pchannel, pquantize)
|
||||||
|
|
||||||
|
# If palette is read only, work on a copy:
|
||||||
|
editable = Gimp.palette_is_editable(palette)
|
||||||
|
if not editable:
|
||||||
|
palette = Gimp.palette_duplicate(palette)
|
||||||
|
|
||||||
|
(exists, num_colors) = Gimp.palette_get_info(palette)
|
||||||
|
|
||||||
|
start, nrows, length = None, None, None
|
||||||
|
if selection == SELECT_AUTOSLICE:
|
||||||
|
def find_index(color, startindex=0):
|
||||||
|
for i in range(startindex, num_colors):
|
||||||
|
c = Gimp.palette_entry_get_color(palette, i)
|
||||||
|
if c[1].r == color[1].r and c[1].g == color[1].g and c[1].b == color[1].b:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
def hexcolor(c):
|
||||||
|
return "#%02x%02x%02x" % (int(255 * c[1].r), int(255 * c[1].b), int(255 * c[1].g))
|
||||||
|
fg = Gimp.context_get_foreground()
|
||||||
|
bg = Gimp.context_get_background()
|
||||||
|
start = find_index(fg)
|
||||||
|
end = find_index(bg)
|
||||||
|
if start is None:
|
||||||
|
raise ValueError("Couldn't find foreground color %s in palette" % hexcolor(fg))
|
||||||
|
if end is None:
|
||||||
|
raise ValueError("Couldn't find background color %s in palette" % hexcolor(bg))
|
||||||
|
if find_index(fg, start + 1):
|
||||||
|
raise ValueError('Autoslice cannot be used when more than one'
|
||||||
|
' instance of an endpoint'
|
||||||
|
' (%s) is present' % hexcolor(fg))
|
||||||
|
if find_index(bg, end + 1):
|
||||||
|
raise ValueError('Autoslice cannot be used when more than one'
|
||||||
|
' instance of an endpoint'
|
||||||
|
' (%s) is present' % hexcolor(bg))
|
||||||
|
if start > end:
|
||||||
|
end, start = start, end
|
||||||
|
length = (end - start) + 1
|
||||||
|
try:
|
||||||
|
_, nrows, _ = parse_slice(slice_expr, length)
|
||||||
|
nrows = abs(nrows)
|
||||||
|
if length % nrows:
|
||||||
|
raise ValueError('Total length %d not evenly divisible'
|
||||||
|
' by number of rows %d' % (length, nrows))
|
||||||
|
length //= nrows
|
||||||
|
except ValueError:
|
||||||
|
# bad expression is okay here, just assume one row
|
||||||
|
nrows = 1
|
||||||
|
# remaining behaviour is implemented by SELECT_SLICE 'inheritance'.
|
||||||
|
selection = SELECT_SLICE
|
||||||
|
elif selection in (SELECT_SLICE, SELECT_PARTITIONED):
|
||||||
|
start, nrows, length = parse_slice(slice_expr, num_colors)
|
||||||
|
|
||||||
|
channels_getter_1 = channel_getters[channel1]
|
||||||
|
channels_getter_2 = channel_getters[channel2]
|
||||||
|
|
||||||
|
def get_colors(start, end):
|
||||||
|
result = []
|
||||||
|
for i in range(start, end):
|
||||||
|
entry = (Gimp.palette_entry_get_name (palette, i)[1],
|
||||||
|
Gimp.palette_entry_get_color (palette, i)[1])
|
||||||
|
index1 = channels_getter_1(entry[1], i)
|
||||||
|
index2 = channels_getter_2(entry[1], i)
|
||||||
|
index = ((index1 - (index1 % grain1)) * (1 if ascending1 else -1),
|
||||||
|
(index2 - (index2 % grain2)) * (1 if ascending2 else -1)
|
||||||
|
)
|
||||||
|
result.append((index, entry))
|
||||||
|
return result
|
||||||
|
|
||||||
|
if selection == SELECT_ALL:
|
||||||
|
entry_list = get_colors(0, num_colors)
|
||||||
|
entry_list.sort(key=lambda v: v[0])
|
||||||
|
for i in range(num_colors):
|
||||||
|
Gimp.palette_entry_set_name (palette, i, entry_list[i][1][0])
|
||||||
|
Gimp.palette_entry_set_color (palette, i, entry_list[i][1][1])
|
||||||
|
|
||||||
|
elif selection == SELECT_PARTITIONED:
|
||||||
|
if num_colors < (start + length * nrows) - 1:
|
||||||
|
raise ValueError('Not enough entries in palette to '
|
||||||
|
'sort complete rows! Got %d, expected >=%d' %
|
||||||
|
(num_colors, start + length * nrows))
|
||||||
|
pchannels_getter = channel_getters[pchannel]
|
||||||
|
for row in range(nrows):
|
||||||
|
partition_spans = [1]
|
||||||
|
rowstart = start + (row * length)
|
||||||
|
old_color = Gimp.palette_entry_get_color (palette,
|
||||||
|
rowstart)[1]
|
||||||
|
old_partition = pchannels_getter(old_color, rowstart)
|
||||||
|
old_partition = old_partition - (old_partition % pgrain)
|
||||||
|
for i in range(rowstart + 1, rowstart + length):
|
||||||
|
this_color = Gimp.palette_entry_get_color (palette, i)[1]
|
||||||
|
this_partition = pchannels_getter(this_color, i)
|
||||||
|
this_partition = this_partition - (this_partition % pgrain)
|
||||||
|
if this_partition == old_partition:
|
||||||
|
partition_spans[-1] += 1
|
||||||
|
else:
|
||||||
|
partition_spans.append(1)
|
||||||
|
old_partition = this_partition
|
||||||
|
base = rowstart
|
||||||
|
for size in partition_spans:
|
||||||
|
palette_sort(palette, SELECT_SLICE, '%d:1,%d' % (base, size),
|
||||||
|
channel1, ascending1,
|
||||||
|
channel2, ascending2,
|
||||||
|
quantize, 0, 1.0)
|
||||||
|
base += size
|
||||||
|
else:
|
||||||
|
# SELECT_SLICE and SELECT_AUTOSLICE
|
||||||
|
stride = length
|
||||||
|
if num_colors < (start + stride * nrows) - 1:
|
||||||
|
raise ValueError('Not enough entries in palette to sort '
|
||||||
|
'complete rows! Got %d, expected >=%d' %
|
||||||
|
(num_colors, start + stride * nrows))
|
||||||
|
|
||||||
|
for row_start in range(start, start + stride * nrows, stride):
|
||||||
|
sublist = get_colors(row_start, row_start + stride)
|
||||||
|
sublist.sort(key=lambda v: v[0])
|
||||||
|
for i, entry in zip(range(row_start, row_start + stride), sublist):
|
||||||
|
Gimp.palette_entry_set_name (palette, i, entry[1][0])
|
||||||
|
Gimp.palette_entry_set_color (palette, i, entry[1][1])
|
||||||
|
|
||||||
|
return palette
|
||||||
|
|
||||||
|
|
||||||
|
selections_option = [ _("All"), _("Slice / Array"), _("Autoslice (fg->bg)"), _("Partitioned") ]
|
||||||
|
class PaletteSort (Gimp.PlugIn):
|
||||||
|
## Parameters ##
|
||||||
|
__gproperties__ = {
|
||||||
|
"run-mode": (Gimp.RunMode,
|
||||||
|
"Run mode",
|
||||||
|
"The run mode",
|
||||||
|
Gimp.RunMode.NONINTERACTIVE,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
# TODO. originally was: (PF_PALETTE, "palette", _("Palette"), ""),
|
||||||
|
# Should probably be of type Gimp.Palette .
|
||||||
|
"palette": (str,
|
||||||
|
_("Palette"),
|
||||||
|
"Palette",
|
||||||
|
"",
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
"selections": (int,
|
||||||
|
_("Se_lections"),
|
||||||
|
str(selections_option),
|
||||||
|
0, 3, 0,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
# TODO: It would be much simpler to replace the slice expression with three
|
||||||
|
# separate parameters: start-index, number-of-rows, row_length
|
||||||
|
"slice_expr": (str,
|
||||||
|
_("Slice _expression"),
|
||||||
|
slice_expr_doc,
|
||||||
|
"",
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
# TODO: was (PF_OPTION, "channel1", _("Channel to _sort"), 3, AVAILABLE_CHANNELS),
|
||||||
|
# Not sure how to implement in gimp3.
|
||||||
|
"channel1": (int,
|
||||||
|
_("Channel to _sort"),
|
||||||
|
"Channel to sort: " + str(AVAILABLE_CHANNELS),
|
||||||
|
0, len(AVAILABLE_CHANNELS), 3,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
"ascending1": (bool,
|
||||||
|
_("_Ascending"),
|
||||||
|
"Ascending",
|
||||||
|
True,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
# TODO: was (PF_OPTION, "channel2", _("Secondary Channel to s_ort"), 5,
|
||||||
|
# AVAILABLE_CHANNELS),
|
||||||
|
"channel2": (int,
|
||||||
|
_("Secondary Channel to s_ort"),
|
||||||
|
"Secondary Channel to sort: " + str(AVAILABLE_CHANNELS),
|
||||||
|
0, len(AVAILABLE_CHANNELS), 5,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
"ascending2": (bool,
|
||||||
|
_("_Ascending"),
|
||||||
|
"Ascending",
|
||||||
|
True,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
"quantize": (float,
|
||||||
|
_("_Quantization"),
|
||||||
|
"Quantization",
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
# TODO: was (PF_OPTION, "pchannel", _("_Partitioning channel"), 3, AVAILABLE_CHANNELS),
|
||||||
|
"pchannel": (int,
|
||||||
|
_("_Partitioning channel"),
|
||||||
|
"Partitioning channel: " + str(AVAILABLE_CHANNELS),
|
||||||
|
0, len(AVAILABLE_CHANNELS), 3,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
"pquantize": (float,
|
||||||
|
_("Partition q_uantization"),
|
||||||
|
"Partition quantization",
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
|
||||||
|
# Returned value
|
||||||
|
"new_palette": (str,
|
||||||
|
_("Palette"),
|
||||||
|
"Palette",
|
||||||
|
"",
|
||||||
|
GObject.ParamFlags.READWRITE),
|
||||||
|
}
|
||||||
|
|
||||||
|
## GimpPlugIn virtual methods ##
|
||||||
|
def do_query_procedures(self):
|
||||||
|
self.set_translation_domain("gimp30-python",
|
||||||
|
Gio.file_new_for_path(Gimp.locale_directory()))
|
||||||
|
return ["python-fu-palette-sort"]
|
||||||
|
|
||||||
|
def do_create_procedure(self, name):
|
||||||
|
procedure = None
|
||||||
|
if name == "python-fu-palette-sort":
|
||||||
|
procedure = Gimp.Procedure.new(self, name,
|
||||||
|
Gimp.PDBProcType.PLUGIN,
|
||||||
|
self.run, None)
|
||||||
|
procedure.set_menu_label(N_("_Sort Palette..."))
|
||||||
|
|
||||||
|
# FIXME: Write humanly readable help -
|
||||||
|
# See for reference: https://gitlab.gnome.org/GNOME/gimp/-/issues/4368#note_763460
|
||||||
|
procedure.set_documentation(
|
||||||
|
N_("Sort the colors in a palette"),
|
||||||
|
dedent("""\
|
||||||
|
palette_sort (palette, selection, slice_expr, channel,
|
||||||
|
channel2, quantize, ascending, pchannel, pquantize) -> new_palette
|
||||||
|
Sorts a palette, or part of a palette, using several options.
|
||||||
|
One can select two color channels over which to sort,
|
||||||
|
and several auxiliary parameters create a 2D sorted
|
||||||
|
palette with sorted rows, among other things.
|
||||||
|
One can optionally install colormath
|
||||||
|
(https://pypi.python.org/pypi/colormath/1.0.8)
|
||||||
|
to GIMP's Python to get even more channels to choose from.
|
||||||
|
"""),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
procedure.set_attribution("João S. O. Bueno, Carol Spears, David Gowers",
|
||||||
|
"João S. O. Bueno, Carol Spears, David Gowers",
|
||||||
|
"2006-2014")
|
||||||
|
procedure.add_menu_path ('<Palettes>')
|
||||||
|
|
||||||
|
procedure.add_argument_from_property(self, "run-mode")
|
||||||
|
procedure.add_argument_from_property(self, "palette")
|
||||||
|
procedure.add_argument_from_property(self, "selections")
|
||||||
|
procedure.add_argument_from_property(self, "slice_expr")
|
||||||
|
procedure.add_argument_from_property(self, "channel1")
|
||||||
|
procedure.add_argument_from_property(self, "ascending1")
|
||||||
|
procedure.add_argument_from_property(self, "channel2")
|
||||||
|
procedure.add_argument_from_property(self, "ascending2")
|
||||||
|
procedure.add_argument_from_property(self, "quantize")
|
||||||
|
procedure.add_argument_from_property(self, "pchannel")
|
||||||
|
procedure.add_argument_from_property(self, "pquantize")
|
||||||
|
procedure.add_return_value_from_property(self, "new_palette")
|
||||||
|
|
||||||
|
return procedure
|
||||||
|
|
||||||
|
def run(self, procedure, args, data):
|
||||||
|
run_mode = args.index(0)
|
||||||
|
palette = args.index(1)
|
||||||
|
selection = args.index(2)
|
||||||
|
slice_expr = args.index(3)
|
||||||
|
channel1 = args.index(4)
|
||||||
|
ascending1 = args.index(5)
|
||||||
|
channel2 = args.index(6)
|
||||||
|
ascending2 = args.index(7)
|
||||||
|
quantize = args.index(8)
|
||||||
|
pchannel = args.index(9)
|
||||||
|
pquantize = args.index(10)
|
||||||
|
|
||||||
|
if palette == '' or palette is None:
|
||||||
|
palette = Gimp.context_get_palette()
|
||||||
|
(exists, num_colors) = Gimp.palette_get_info(palette)
|
||||||
|
if not exists:
|
||||||
|
error = 'Unknown palette: {}'.format(palette)
|
||||||
|
return procedure.new_return_values(Gimp.PDBStatusType.CALLING_ERROR,
|
||||||
|
GLib.Error(error))
|
||||||
|
|
||||||
|
# TODO: Add UI
|
||||||
|
try:
|
||||||
|
new_palette = palette_sort(palette, selection, slice_expr, channel1, ascending1,
|
||||||
|
channel2, ascending2, quantize, pchannel, pquantize)
|
||||||
|
except ValueError as err:
|
||||||
|
return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR,
|
||||||
|
GLib.Error(str(err)))
|
||||||
|
|
||||||
|
return_val = procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
|
||||||
|
value = GObject.Value(GObject.TYPE_STRING, new_palette)
|
||||||
|
return_val.remove(1)
|
||||||
|
return_val.insert(1, value)
|
||||||
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
|
Gimp.main(PaletteSort.__gtype__, sys.argv)
|
Reference in New Issue
Block a user