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