1#!/usr/local/bin/python
2# -*- coding: utf-8 -*-
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17from gimpfu import *
18# little known, colorsys is part of Python's stdlib
19from colorsys import rgb_to_yiq
20from textwrap import dedent
21from random import randint
22
23gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
24
25AVAILABLE_CHANNELS = (_("Red"), _("Green"), _("Blue"),
26                      _("Luma (Y)"),
27                      _("Hue"), _("Saturation"), _("Value"),
28                      _("Saturation (HSL)"), _("Lightness (HSL)"),
29                      _("Index"),
30                      _("Random"))
31
32GRAIN_SCALE = (1.0, 1.0 , 1.0,
33              1.0,
34              360., 100., 100.,
35              100., 100.,
36              16384.,
37              float(0x7ffffff),
38              100., 256., 256.,
39              256., 360.,)
40
41SELECT_ALL = 0
42SELECT_SLICE = 1
43SELECT_AUTOSLICE = 2
44SELECT_PARTITIONED = 3
45SELECTIONS = (SELECT_ALL, SELECT_SLICE, SELECT_AUTOSLICE, SELECT_PARTITIONED)
46
47
48def noop(v, i):
49    return v
50
51
52def to_hsv(v, i):
53    return v.to_hsv()
54
55
56def to_hsl(v, i):
57    return v.to_hsl()
58
59
60def to_yiq(v, i):
61    return rgb_to_yiq(*v[:-1])
62
63
64def to_index(v, i):
65    return (i,)
66
67def to_random(v, i):
68    return (randint(0, 0x7fffffff),)
69
70
71channel_getters = [ (noop, 0), (noop, 1), (noop, 2),
72                    (to_yiq, 0),
73                    (to_hsv, 0), (to_hsv, 1), (to_hsv, 2),
74                    (to_hsl, 1), (to_hsl, 2),
75                    (to_index, 0),
76                    (to_random, 0)]
77
78
79try:
80    from colormath.color_objects import RGBColor, LabColor, LCHabColor
81    AVAILABLE_CHANNELS = AVAILABLE_CHANNELS + (_("Lightness (LAB)"),
82                                               _("A-color"), _("B-color"),
83                                               _("Chroma (LCHab)"),
84                                               _("Hue (LCHab)"))
85    to_lab = lambda v,i: RGBColor(*v[:-1]).convert_to('LAB').get_value_tuple()
86    to_lchab = (lambda v,i:
87                    RGBColor(*v[:-1]).convert_to('LCHab').get_value_tuple())
88    channel_getters.extend([(to_lab, 0), (to_lab, 1), (to_lab, 2),
89                            (to_lchab, 1), (to_lchab, 2)])
90except ImportError:
91    pass
92
93
94def parse_slice(s, numcolors):
95    """Parse a slice spec and return (start, nrows, length)
96    All items are optional. Omitting them makes the largest possible selection that
97    exactly fits the other items.
98
99    start:nrows,length
100
101
102    '' selects all items, as does ':'
103    ':4,' makes a 4-row selection out of all colors (length auto-determined)
104    ':4' also.
105    ':1,4' selects the first 4 colors
106    ':,4' selects rows of 4 colors (nrows auto-determined)
107    ':4,4' selects 4 rows of 4 colors
108    '4:' selects a single row of all colors after 4, inclusive.
109    '4:,4' selects rows of 4 colors, starting at 4 (nrows auto-determined)
110    '4:4,4' selects 4 rows of 4 colors (16 colors total), beginning at index 4.
111    '4' is illegal (ambiguous)
112
113
114    In general, slices are comparable to a numpy sub-array.
115    'start at element START, with shape (NROWS, LENGTH)'
116
117    """
118    s = s.strip()
119
120    def notunderstood():
121        raise ValueError('Slice %r not understood. Should be in format'
122                         ' START?:NROWS?,ROWLENGTH? eg. "0:4,16".' % s)
123    def _int(v):
124        try:
125            return int(v)
126        except ValueError:
127            notunderstood()
128    if s in ('', ':', ':,'):
129        return 0, 1, numcolors # entire palette, one row
130    if s.count(':') != 1:
131        notunderstood()
132    rowpos = s.find(':')
133    start = 0
134    if rowpos > 0:
135        start = _int(s[:rowpos])
136    numcolors -= start
137    nrows = 1
138    if ',' in s:
139        commapos = s.find(',')
140        nrows = s[rowpos+1:commapos]
141        length = s[commapos+1:]
142        if not nrows:
143            if not length:
144                notunderstood()
145            else:
146                length = _int(length)
147                if length == 0:
148                    notunderstood()
149                nrows = numcolors // length
150                if numcolors % length:
151                    nrows = -nrows
152        elif not length:
153            nrows = _int(nrows)
154            if nrows == 0:
155                notunderstood()
156            length = numcolors // nrows
157            if numcolors % nrows:
158                length = -length
159        else:
160            nrows = _int(nrows)
161            if nrows == 0:
162                notunderstood()
163            length = _int(length)
164            if length == 0:
165                notunderstood()
166    else:
167        nrows = _int(s[rowpos+1:])
168        if nrows == 0:
169            notunderstood()
170        length = numcolors // nrows
171        if numcolors % nrows:
172            length = -length
173    return start, nrows, length
174
175
176def quantization_grain(channel, g):
177    "Given a channel and a quantization, return the size of a quantization grain"
178    g = max(1.0, g)
179    if g <= 1.0:
180        g = 0.00001
181    else:
182        g = max(0.00001, GRAIN_SCALE[channel] / g)
183    return g
184
185
186def palette_sort(palette, selection, slice_expr, channel1, ascending1,
187                 channel2, ascending2, quantize, pchannel, pquantize):
188
189    grain1 = quantization_grain(channel1, quantize)
190    grain2 = quantization_grain(channel2, quantize)
191    pgrain = quantization_grain(pchannel, pquantize)
192
193    #If palette is read only, work on a copy:
194    editable = pdb.gimp_palette_is_editable(palette)
195    if not editable:
196        palette = pdb.gimp_palette_duplicate (palette)
197
198    num_colors = pdb.gimp_palette_get_info (palette)
199
200    start, nrows, length = None, None, None
201    if selection == SELECT_AUTOSLICE:
202        def find_index(color, startindex=0):
203            for i in range(startindex, num_colors):
204                c = pdb.gimp_palette_entry_get_color (palette, i)
205                if c == color:
206                    return i
207            return None
208        def hexcolor(c):
209            return "#%02x%02x%02x" % tuple(c[:-1])
210        fg = pdb.gimp_context_get_foreground()
211        bg = pdb.gimp_context_get_background()
212        start = find_index(fg)
213        end = find_index(bg)
214        if start is None:
215            raise ValueError("Couldn't find foreground color %r in palette" % list(fg))
216        if end is None:
217            raise ValueError("Couldn't find background color %r in palette" % list(bg))
218        if find_index(fg, start + 1):
219            raise ValueError('Autoslice cannot be used when more than one'
220                             ' instance of an endpoint'
221                             ' (%s) is present' % hexcolor(fg))
222        if find_index(bg, end + 1):
223            raise ValueError('Autoslice cannot be used when more than one'
224                             ' instance of an endpoint'
225                             ' (%s) is present' % hexcolor(bg))
226        if start > end:
227            end, start = start, end
228        length = (end - start) + 1
229        try:
230            _, nrows, _ = parse_slice(slice_expr, length)
231            nrows = abs(nrows)
232            if length % nrows:
233                raise ValueError('Total length %d not evenly divisible'
234                                 ' by number of rows %d' % (length, nrows))
235            length /= nrows
236        except ValueError:
237            # bad expression is okay here, just assume one row
238            nrows = 1
239        # remaining behaviour is implemented by SELECT_SLICE 'inheritance'.
240        selection= SELECT_SLICE
241    elif selection in (SELECT_SLICE, SELECT_PARTITIONED):
242        start, nrows, length = parse_slice(slice_expr, num_colors)
243
244    channels_getter_1, channel_index = channel_getters[channel1]
245    channels_getter_2, channel2_index = channel_getters[channel2]
246
247    def get_colors(start, end):
248        result = []
249        for i in range(start, end):
250            entry =  (pdb.gimp_palette_entry_get_name (palette, i),
251                      pdb.gimp_palette_entry_get_color (palette, i))
252            index1 = channels_getter_1(entry[1], i)[channel_index]
253            index2 = channels_getter_2(entry[1], i)[channel2_index]
254            index = ((index1 - (index1 % grain1)) * (1 if ascending1 else -1),
255                     (index2 - (index2 % grain2)) * (1 if ascending2 else -1)
256                    )
257            result.append((index, entry))
258        return result
259
260    if selection == SELECT_ALL:
261        entry_list = get_colors(0, num_colors)
262        entry_list.sort(key=lambda v:v[0])
263        for i in range(num_colors):
264            pdb.gimp_palette_entry_set_name (palette, i, entry_list[i][1][0])
265            pdb.gimp_palette_entry_set_color (palette, i, entry_list[i][1][1])
266
267    elif selection == SELECT_PARTITIONED:
268        if num_colors < (start + length * nrows) - 1:
269            raise ValueError('Not enough entries in palette to '
270                             'sort complete rows! Got %d, expected >=%d' %
271                             (num_colors, start + length * nrows))
272        pchannels_getter, pchannel_index = channel_getters[pchannel]
273        for row in range(nrows):
274            partition_spans = [1]
275            rowstart = start + (row * length)
276            old_color = pdb.gimp_palette_entry_get_color (palette,
277                                                          rowstart)
278            old_partition = pchannels_getter(old_color, rowstart)[pchannel_index]
279            old_partition = old_partition - (old_partition % pgrain)
280            for i in range(rowstart + 1, rowstart + length):
281                this_color = pdb.gimp_palette_entry_get_color (palette, i)
282                this_partition = pchannels_getter(this_color, i)[pchannel_index]
283                this_partition = this_partition - (this_partition % pgrain)
284                if this_partition == old_partition:
285                    partition_spans[-1] += 1
286                else:
287                    partition_spans.append(1)
288                    old_partition = this_partition
289            base = rowstart
290            for size in partition_spans:
291                palette_sort(palette, SELECT_SLICE, '%d:1,%d' % (base, size),
292                             channel, quantize, ascending, 0, 1.0)
293                base += size
294    else:
295        stride = length
296        if num_colors < (start + stride * nrows) - 1:
297            raise ValueError('Not enough entries in palette to sort '
298                             'complete rows! Got %d, expected >=%d' %
299                             (num_colors, start + stride * nrows))
300
301        for row_start in range(start, start + stride * nrows, stride):
302            sublist = get_colors(row_start, row_start + stride)
303            sublist.sort(key=lambda v:v[0], reverse=not ascending)
304            for i, entry in zip(range(row_start, row_start + stride), sublist):
305                pdb.gimp_palette_entry_set_name (palette, i, entry[1][0])
306                pdb.gimp_palette_entry_set_color (palette, i, entry[1][1])
307
308    return palette
309
310register(
311    "python-fu-palette-sort",
312    N_("Sort the colors in a palette"),
313    # FIXME: Write humanly readable help -
314    # (I can't figure out what the plugin does, or how to use the parameters after
315    # David's enhancements even looking at the code -
316    # let alone someone just using GIMP (JS) )
317    dedent("""\
318    palette_sort (palette, selection, slice_expr, channel,
319    channel2, quantize, ascending, pchannel, pquantize) -> new_palette
320    Sorts a palette, or part of a palette, using several options.
321    One can select two color channels over which to sort,
322    and several auxiliary parameters create a 2D sorted
323    palette with sorted rows, among other things.
324    One can optionally install colormath
325    (https://pypi.python.org/pypi/colormath/1.0.8)
326    to GIMP's Python to get even more channels to choose from.
327    """),
328    "João S. O. Bueno, Carol Spears, David Gowers",
329    "João S. O. Bueno, Carol Spears, David Gowers",
330    "2006-2014",
331    N_("_Sort Palette..."),
332    "",
333    [
334        (PF_PALETTE, "palette",  _("Palette"), ""),
335        (PF_OPTION, "selections", _("Se_lections"), SELECT_ALL,
336                    (_("All"), _("Slice / Array"), _("Autoslice (fg->bg)"),
337                     _("Partitioned"))),
338        (PF_STRING,    "slice-expr", _("Slice _expression"), ''),
339        (PF_OPTION,   "channel1",    _("Channel to _sort"), 3,
340                                    AVAILABLE_CHANNELS),
341        (PF_BOOL,  "ascending1", _("_Ascending"), True),
342        (PF_OPTION,   "channel2",    _("Secondary Channel to s_ort"), 5,
343                                    AVAILABLE_CHANNELS),
344        (PF_BOOL,   "ascending2", _("_Ascending"), True),
345        (PF_FLOAT,  "quantize", _("_Quantization"), 0.0),
346        (PF_OPTION,   "pchannel",    _("_Partitioning channel"), 3,
347                                            AVAILABLE_CHANNELS),
348        (PF_FLOAT,  "pquantize", _("Partition q_uantization"), 0.0),
349    ],
350    [],
351    palette_sort,
352    menu="<Palettes>",
353    domain=("gimp20-python", gimp.locale_directory)
354    )
355
356main ()
357