├── README.md ├── flifcrush.py ├── old_flifcrush └── samples ├── FreedroidRPG.png └── screenshot.png /README.md: -------------------------------------------------------------------------------- 1 | # flifcrush 2 | ## bruteforce flif optimizer 3 | 4 | ### Usage 5 | 6 | ```` 7 | flifcrush.py [-h] [-i] [-n] [-d] [-c] N1 N2 N3 [Nx ...] 8 | ```` 9 | 10 | ```` 11 | N file(s) or path (recursively) to be converted to flif 12 | 13 | optional arguments: 14 | -h, --help show help message and exit 15 | -i, --interlace force interlacing (default: find out best) 16 | -n, --nointerlace force interlacing off (default: find out best) 17 | -d, --debug print output of all runs at end 18 | -c, --compare compare to default flif compression 19 | ```` 20 | 21 | #### Example 22 | ```` 23 | ./flifcrush.py -c samples/FreedroidRPG.png 24 | ```` 25 | 26 | ![Usage Example](https://raw.githubusercontent.com/matthiaskrgr/flifcrush/master/samples/screenshot.png) 27 | -------------------------------------------------------------------------------- /flifcrush.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # flifcrush - tries to reduce FLIF files in size 4 | # Copyright (C) 2015 - 2017 Matthias Krüger 5 | 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 1, or (at your option) 9 | # any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 19 | 20 | import sys 21 | if (sys.version_info.major != 3): # no python 3 22 | print("Python 3 or higher is required.") 23 | sys.exit(1) 24 | 25 | import subprocess 26 | import os 27 | from PIL import Image # https://github.com/python-pillow/Pillow 28 | from collections import Counter 29 | from collections import namedtuple 30 | import argparse 31 | from itertools import chain # combine ranges 32 | 33 | import random # getrandomfilename 34 | import string # getrandomfilename 35 | __author__ = 'Matthias "matthiaskrgr" Krüger' 36 | 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument("inpath", help="file or path (recursively) to be converted to flif", metavar='N', nargs='+', type=str) 39 | parser.add_argument("-i", "--interlace", help="force interlacing (default: find out best)", action='store_true') 40 | parser.add_argument("-n", "--nointerlace", help="force interlacing off (default: find out best)", action='store_true') 41 | parser.add_argument("-d", "--debug", help="print output of all runs at end", action='store_true') 42 | #parser.add_argument("-b", "--bruteforce", help="bruteforce compression values, takes AGES and might be outdated", action='store_true') 43 | parser.add_argument("-c", "--compare", help="compare to default flif compression", action='store_true') 44 | args = parser.parse_args() 45 | 46 | COMPARE = (args.compare) 47 | DEBUG = (args.debug) 48 | INPATHS = args.inpath 49 | 50 | interlace_flag="--no-interlace" # default: false 51 | INTERLACE=False 52 | INTERLACE_FORCE=False 53 | 54 | # make these global to access them easily inside functions 55 | global size_before_glob, size_after_glob, files_count_glob, size_flifdefault_glob 56 | size_before_glob = 0 # size of all images we process 57 | size_after_glob = 0 # size of all flifs we generated 58 | files_count_glob = 0 # number of files 59 | size_flifdefault_glob = 0 # size of all images converted with flif default parameters 60 | 61 | 62 | # colors for stdout 63 | txt_ul = TXT_UL = '\033[04m' # underline 64 | txt_res = TXT_RES = '\033[0m' #reset 65 | 66 | if args.interlace: 67 | interlace_flag="--interlace" 68 | INTERLACE=True 69 | INTERLACE_FORCE=True # do we force true or false? 70 | best_interl = True 71 | 72 | if args.nointerlace: 73 | interlace_flag="--no-interlace" 74 | INTERLACE=False 75 | INTERLACE_FORCE=True # do we force true or false? 76 | best_interl = False 77 | 78 | #BRUTEFORCE = (args.bruteforce) 79 | global output_best 80 | output_best="none" 81 | global arr_index 82 | global progress_array 83 | arr_index = 0 84 | #progress_array="|/-\\" 85 | #progress_array=".o0OOo." 86 | progress_array=" ▁▂▃▄▅▆▇█▇▆▅▄▃▁" 87 | arrlen=len(progress_array) 88 | 89 | # prints activity indicator (some kind of ascii 'animation') 90 | def showActivity(func_arg, size_new): 91 | global arr_index 92 | arr_index+=1 93 | if (arr_index == arrlen): 94 | arr_index = 0 95 | # diff_best = best_dict['size'] - size_new 96 | 97 | print(progress_array[arr_index] + " " + str(count) + ": " + str(func_arg) + ", size: " + str(size_new) + " b ", end="\r",flush=True) 98 | 99 | # save .flif file that had the best combination of parameters 100 | def save_file(): 101 | global output_best 102 | flif2flif = False # default, we need extra parameter if we convert .flif to -clif 103 | # if the condition is false, we didn't manage to reduce size 104 | if output_best != "none": 105 | OUTFILE=".".join(INFILE.split(".")[:-1])+".flif" # split by ".", rm last elm, join by "." and add "flif" extension 106 | 107 | if (OUTFILE == INFILE): # most likely flif fo flif crushing 108 | flif2flif = True 109 | OUTFILE=get_rand_filename() 110 | 111 | with open(OUTFILE, "w+b") as f: 112 | f.write(output_best) 113 | f.close 114 | 115 | size_flif=os.path.getsize(OUTFILE) 116 | size_orig=os.path.getsize(INFILE) 117 | 118 | if (flif2flif): # overwrite INFILE with OUTFILE 119 | os.remove(INFILE) 120 | os.rename(OUTFILE, INFILE) # rename outfile to infile 121 | 122 | # print some numbers 123 | global size_after_glob 124 | size_after_glob += size_flif 125 | size_diff = size_orig - best_dict['size'] 126 | 127 | print("\033[K", end="") 128 | print("reduced from " + str(size_orig) + " b to "+ str(best_dict['size']) + " ( -"+ str(size_diff) + " b, "+ str((( best_dict['size'] - size_orig)/ size_orig )*100)[:6] + " %) " + str(count) + " flif calls.\n\n") 129 | #print("reduced from {size_orig}b to {size_flif}b ({size_diff}b, {perc_change} %) via \n [{bestoptim}] and {cnt} flif calls.\n\n".format(size_orig = os.path.getsize(INFILE), size_flif=size_flif, size_diff=(size_flif - size_orig), perc_change=str(((size_flif-size_orig) / size_orig)*100)[:6], bestoptim=str("maniac repeats:" + str(best_dict['maniac_repeats']) + " maniac_threshold:" + str(best_dict['maniac_threshold']) + " maniac_min_size:" + str(best_dict['maniac_min_size'])+ " maniac_divisor:" + str(best_dict['maniac_divisor']) + " max_palette_size:" + str(best_dict['max_palette_size']) + " chance-cutoff:" + str(best_dict['chance_cutoff']) + " chance-alpha:" + str(best_dict['chance_alpha']) + " ACB:" + str(best_dict['ACB']) + " INTERLACE:" + str(best_dict['INT']) + " PLC:" + str(best_dict['PLC']) + " RGB:" + str(best_dict['RGB']) + " A:" + str(best_dict['A'])), cnt=str(count)), end="\r") 130 | 131 | if (best_dict['size'] > size_orig): 132 | print("WARNING: failed to reduce size") 133 | 134 | else: 135 | print("\033[K", end="") 136 | print("WARNING: could not reduce size! (" + str(count) + " attempts)") 137 | 138 | # generates a name for a file that does not exist in current directory, used for tmp files 139 | def get_rand_filename(): 140 | # this prevents accidentally overwriting a preexisting file 141 | filename =''.join(random.choice(string.ascii_uppercase) for i in range(9)) 142 | while (os.path.isfile(filename)): # if the name already exists, try again 143 | filename =''.join(random.choice(string.ascii_uppercase) for i in range(9)) 144 | return filename 145 | 146 | def pct_of_best(size_new): 147 | # if best size was 100 and new file is 50, return 50 % 148 | pct = str(((size_new - best_dict['size']) / best_dict['size'])*100) 149 | pct = "-0.000" if ("e" in pct) else pct[:6] # due to too-early [:6], '8.509566454608271e-07' would become "8.509" 150 | return pct 151 | 152 | 153 | def crush_maniac_repeats(): # -N 154 | # globals we modify 155 | global best_dict 156 | global count 157 | global arr_index 158 | global output_best 159 | 160 | # locals 161 | range_maniac_repeats = 20 # try 0 -20 162 | max_attempts = 5 # give up after 5 unsuccesfull attempts 163 | failed_attempts = 0 164 | 165 | for maniac_repeats in range(0, range_maniac_repeats): 166 | count += 1 167 | raw_command = [ 168 | flif_binary, 169 | flif_to_flif, 170 | 171 | ('--maniac-repeats=' + str(maniac_repeats)), # <- 172 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 173 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 174 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 175 | 176 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 177 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 178 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 179 | ('--guess=' + str(best_dict['guess'])), 180 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 181 | 182 | best_dict['interlace'].flag, 183 | best_dict['no_channel_compact'].flag, 184 | best_dict['force_color_buckets'].flag, 185 | best_dict['no_ycocg'].flag, 186 | best_dict['keep_invisible_rgb'].flag, 187 | best_dict['no_subtract_green'].flag, 188 | 189 | INFILE, 190 | interlace_flag, 191 | '--overwrite', 192 | '/dev/stdout' 193 | ] # = raw_command 194 | 195 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 196 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 197 | size_new = sys.getsizeof(output) 198 | showActivity("maniac repeats: " + str(maniac_repeats), size_new) 199 | 200 | #if (DEBUG): 201 | # debug_array.append([{'Nr':count, 'maniac_repeats':maniac_repeats, 'maniac_threshold':maniac_threshold, 'maniac_min_size':maniac_min_size, 'maniac_divisor':maniac_divisor, 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 202 | 203 | 204 | if ((best_dict['size'] > size_new)): # new file is smaller // count==1: make sure best_dict is filled with first values we obtain. this way we still continue crushing even if initial N-run does not reduce size smaller than size_orig 205 | failed_attempts = 0 # reset break-counter 206 | output_best = output 207 | 208 | size_change = best_dict['size']-size_new 209 | perc_change = pct_of_best(size_new) 210 | print("\033[K", end="") 211 | print( 212 | str(count) + 213 | " maniac [ " + TXT_UL + "repeat: " + str(maniac_repeats) + TXT_RES + 214 | " threshold: " + str(best_dict['maniac_threshold']) + 215 | " min_size: " + str(best_dict['maniac_min_size']) + 216 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 217 | 218 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 219 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 220 | " palette: " + str(best_dict['max_palette_size']) + 221 | 222 | " itlc: " + str(best_dict['interlace'].bool) + 223 | " guess: " + str(best_dict['guess']) + 224 | " inv_guess: " + str(best_dict['invisible_guess']) + 225 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 226 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 227 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 228 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 229 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 230 | 231 | " size " + str(size_new) + " b " + 232 | "-" + str(size_change) + " b " + 233 | perc_change + " %") 234 | 235 | best_dict['size'] = size_new 236 | best_dict['count'] = count 237 | best_dict['maniac_repeats'] = maniac_repeats 238 | arr_index = 0 239 | else: # file is not smaller 240 | failed_attempts += 1 241 | if (failed_attempts >= max_attempts): 242 | return; # break out of loop, we have wasted enough time here 243 | 244 | 245 | def crush_maniac_threshold(): # -T 246 | # globals 247 | global best_dict 248 | global count 249 | global arr_index 250 | global output_best 251 | 252 | if (best_dict['maniac_repeats'] == 0): # nothing to do here 253 | return 254 | 255 | #locals 256 | range_maniac_threshold = 40 257 | max_attempts = 40 258 | failed_attempts = 0 259 | 260 | for maniac_threshold in range(1, range_maniac_threshold): 261 | 262 | # skip maniac_threshold 1-4, it takes too much ram in extreme cases 263 | if (maniac_threshold <= 4): 264 | continue 265 | 266 | count += 1 267 | 268 | raw_command = [ 269 | flif_binary, 270 | flif_to_flif, 271 | 272 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 273 | ('--maniac-threshold=' + str(maniac_threshold)), # <- 274 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 275 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 276 | 277 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 278 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 279 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 280 | ('--guess=' + str(best_dict['guess'])), 281 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 282 | 283 | best_dict['interlace'].flag, 284 | best_dict['no_channel_compact'].flag, 285 | best_dict['force_color_buckets'].flag, 286 | best_dict['no_ycocg'].flag, 287 | best_dict['keep_invisible_rgb'].flag, 288 | best_dict['no_subtract_green'].flag, 289 | 290 | INFILE, 291 | interlace_flag, 292 | '--overwrite', 293 | '/dev/stdout', 294 | ] # = raw_command 295 | 296 | 297 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 298 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 299 | size_new = sys.getsizeof(output) 300 | showActivity(("maniac threshold: " + str(maniac_threshold)), size_new) 301 | 302 | 303 | 304 | #if (DEBUG): 305 | # debug_array.append([{'Nr':count, 'maniac_repeats':best_dict['maniac_repeats'], 'maniac_threshold':maniac_threshold, 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size': max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 306 | 307 | if (best_dict['size'] > size_new): # new file is smaller 308 | failed_attempts = 0 # reset break-counter 309 | output_best = output 310 | size_change = best_dict['size']-size_new 311 | perc_change = pct_of_best(size_new) 312 | print("\033[K", end="") 313 | print( 314 | str(count) + 315 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 316 | " " + TXT_UL + "threshold: " + str(maniac_threshold) + TXT_RES + 317 | " min_size: " + str(best_dict['maniac_min_size']) + 318 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 319 | 320 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 321 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 322 | " palette: " + str(best_dict['max_palette_size']) + 323 | 324 | " itlc: " + str(best_dict['interlace'].bool) + 325 | " guess: " + str(best_dict['guess']) + 326 | " inv_guess: " + str(best_dict['invisible_guess']) + 327 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 328 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 329 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 330 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 331 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 332 | 333 | " size " + str(size_new) + " b " + 334 | "-" + str(size_change) + " b " + 335 | perc_change + " %") 336 | 337 | best_dict['size'] = size_new 338 | best_dict['count'] = count 339 | best_dict['maniac_threshold'] = maniac_threshold 340 | arr_index = 0 341 | else: 342 | failed_attempts += 1 343 | if (failed_attempts >= max_attempts): 344 | return; 345 | 346 | 347 | def crush_maniac_divisor(): # -D 348 | # globals 349 | global best_dict 350 | global count 351 | global arr_index 352 | global output_best 353 | 354 | if (best_dict['maniac_repeats'] == 0): # nothing to do here 355 | return 356 | 357 | #locals 358 | range_maniac_divisor = 268435455 359 | maniac_divisor = 1 360 | maniac_divisor_step = 1 361 | maniac_divisor_step_upped = 0 # if True; maniac_divisor_step == 10 362 | failed_attempts = 0 363 | max_attempts = 200 364 | while (maniac_divisor < range_maniac_divisor): 365 | count +=1 366 | 367 | 368 | raw_command = [ 369 | flif_binary, 370 | flif_to_flif, 371 | 372 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 373 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 374 | ('--maniac-divisor=' + str(maniac_divisor)), # <- 375 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 376 | 377 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 378 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 379 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 380 | ('--guess=' + str(best_dict['guess'])), 381 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 382 | 383 | best_dict['interlace'].flag, 384 | best_dict['no_channel_compact'].flag, 385 | best_dict['force_color_buckets'].flag, 386 | best_dict['no_ycocg'].flag, 387 | best_dict['keep_invisible_rgb'].flag, 388 | best_dict['no_subtract_green'].flag, 389 | 390 | INFILE, 391 | interlace_flag, 392 | '--overwrite', 393 | '/dev/stdout', 394 | ] # = raw_command 395 | 396 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 397 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 398 | size_new = sys.getsizeof(output) 399 | showActivity("maniac divisor: " + str(maniac_divisor), size_new) 400 | 401 | #if (DEBUG): 402 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size': max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 403 | 404 | 405 | if (best_dict['size'] > size_new): # new file is smaller 406 | failed_attempts = 0 # reset break-counter 407 | output_best = output 408 | size_change = best_dict['size']-size_new 409 | perc_change = pct_of_best(size_new) 410 | print("\033[K", end="") 411 | print( 412 | str(count) + 413 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 414 | " threshold: " + str(best_dict['maniac_threshold']) + 415 | " min_size: " + str(best_dict['maniac_min_size']) + 416 | " " + TXT_UL + "divisor: " + str(maniac_divisor) + TXT_RES + " ] " + # ] maniac <---- 417 | 418 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 419 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 420 | " palette: " + str(best_dict['max_palette_size']) + 421 | 422 | " itlc: " + str(best_dict['interlace'].bool) + 423 | " guess: " + str(best_dict['guess']) + 424 | " inv_guess: " + str(best_dict['invisible_guess']) + 425 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 426 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 427 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 428 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 429 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 430 | 431 | " size " + str(size_new) + " b " + 432 | "-" + str(size_change) + " b " + 433 | perc_change + " %") 434 | 435 | best_dict['size'] = size_new 436 | best_dict['count'] = count 437 | best_dict['maniac_divisor'] = maniac_divisor 438 | arr_index=0 439 | else: 440 | failed_attempts += 1 441 | if ((maniac_divisor >= 100) and (maniac_divisor_step_upped == 0)): # increase the loop stepping to speed things up 442 | maniac_divisor_step = 10 443 | maniac_divisor_step_upped = 1 444 | if ((maniac_divisor >= 1000) and (maniac_divisor_step_upped == 1)): 445 | maniac_divisor_step = 100 446 | maniac_divisor_step_upped = 2 447 | if ((maniac_divisor >= 5000) and (maniac_divisor_step_upped == 2)): 448 | maniac_divisor_step = 1000 449 | maniac_divisor_step_upped = 3 450 | if ((maniac_divisor >= 13000) and (maniac_divisor_step_upped == 3)): 451 | maniac_divisor_step = 10000 452 | maniac_divisor_step_upped = 4 453 | if (failed_attempts >= max_attempts): 454 | if (maniac_divisor < 268435453): # try max maniac_divisor 455 | maniac_divisor = 268435454 456 | continue 457 | break; 458 | 459 | if (maniac_divisor >= range_maniac_divisor): 460 | break 461 | maniac_divisor += maniac_divisor_step 462 | 463 | 464 | def crush_maniac_min_size(): # -M 465 | # globals 466 | global best_dict 467 | global count 468 | global arr_index 469 | global output_best 470 | 471 | if (best_dict['maniac_repeats'] == 0): # nothing to do here 472 | return 473 | 474 | #locals 475 | range_maniac_min_size = 3000 476 | max_attempts = 200 477 | failed_attempts = 0 478 | 479 | 480 | for maniac_min_size in range(0, range_maniac_min_size): 481 | count +=1 482 | 483 | #if (DEBUG): 484 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size': max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 485 | 486 | raw_command = [ 487 | flif_binary, 488 | flif_to_flif, 489 | 490 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 491 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 492 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 493 | ('--maniac-min-size=' + str(maniac_min_size)), # <- 494 | 495 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 496 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 497 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 498 | ('--guess=' + str(best_dict['guess'])), 499 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 500 | 501 | best_dict['interlace'].flag, 502 | best_dict['no_channel_compact'].flag, 503 | best_dict['force_color_buckets'].flag, 504 | best_dict['no_ycocg'].flag, 505 | best_dict['keep_invisible_rgb'].flag, 506 | best_dict['no_subtract_green'].flag, 507 | 508 | INFILE, 509 | interlace_flag, 510 | '--overwrite', 511 | '/dev/stdout', 512 | ] # = raw_command 513 | 514 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 515 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 516 | size_new = sys.getsizeof(output) 517 | showActivity("maniac min size: " + str(maniac_min_size), size_new) 518 | 519 | #if (DEBUG): 520 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size': max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 521 | 522 | 523 | if (best_dict['size'] > size_new): # new file is smaller 524 | failed_attempts = 0 # reset break-counter 525 | output_best = output 526 | size_change = best_dict['size']-size_new 527 | perc_change = pct_of_best(size_new) 528 | print("\033[K", end="") 529 | print( 530 | str(count) + 531 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 532 | " threshold: " + str(best_dict['maniac_threshold']) + 533 | " " + TXT_UL + "min_size: " + str(maniac_min_size) + TXT_RES + # <---- 534 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 535 | 536 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 537 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 538 | " palette: " + str(best_dict['max_palette_size']) + 539 | 540 | " itlc: " + str(best_dict['interlace'].bool) + 541 | " guess: " + str(best_dict['guess']) + 542 | " inv_guess: " + str(best_dict['invisible_guess']) + 543 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 544 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 545 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 546 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 547 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 548 | 549 | " size " + str(size_new) + " b " + 550 | "-" + str(size_change) + " b " + 551 | perc_change + " %") 552 | 553 | best_dict['maniac_min_size'] = maniac_min_size 554 | best_dict['size'] = size_new 555 | best_dict['count'] = count 556 | failed_attempts = 0 557 | arr_index = 0 558 | else: 559 | failed_attempts += 1 560 | if (failed_attempts >= max_attempts): 561 | break; 562 | 563 | 564 | 565 | def crush_chance_cutoff(): 566 | # globals 567 | global best_dict 568 | global count 569 | global arr_index 570 | global output_best 571 | 572 | #locals 573 | range_chance_cutoff = 128 574 | failed_attempts = 0 575 | max_attempts=200 576 | 577 | for chance_cutoff in range(1, range_chance_cutoff): 578 | count += 1 579 | 580 | raw_command = [ 581 | flif_binary, 582 | flif_to_flif, 583 | 584 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 585 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 586 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 587 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 588 | 589 | ('--chance-cutoff=' + str(chance_cutoff)), # <- 590 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 591 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 592 | ('--guess=' + str(best_dict['guess'])), 593 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 594 | 595 | best_dict['interlace'].flag, 596 | best_dict['no_channel_compact'].flag, 597 | best_dict['force_color_buckets'].flag, 598 | best_dict['no_ycocg'].flag, 599 | best_dict['keep_invisible_rgb'].flag, 600 | best_dict['no_subtract_green'].flag, 601 | 602 | INFILE, 603 | interlace_flag, 604 | '--overwrite', 605 | '/dev/stdout', 606 | ] # = raw_command 607 | 608 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 609 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 610 | size_new = sys.getsizeof(output) 611 | showActivity("chance cutoff: " + str(chance_cutoff), size_new) 612 | 613 | 614 | #if (DEBUG): 615 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 616 | 617 | 618 | if (best_dict['size'] > size_new): # new file is smaller 619 | failed_attempts = 0 # reset break-counter 620 | output_best = output 621 | size_change = best_dict['size']-size_new 622 | perc_change = pct_of_best(size_new) 623 | print("\033[K", end="") 624 | print( 625 | str(count) + 626 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 627 | " threshold: " + str(best_dict['maniac_threshold']) + 628 | " min_size: " + str(best_dict['maniac_min_size']) + 629 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 630 | 631 | " chance:[ "+ TXT_UL + "cutoff: " + str(chance_cutoff) + TXT_RES + # <---- 632 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 633 | " palette: " + str(best_dict['max_palette_size']) + 634 | 635 | " itlc: " + str(best_dict['interlace'].bool) + 636 | " guess: " + str(best_dict['guess']) + 637 | " inv_guess: " + str(best_dict['invisible_guess']) + 638 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 639 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 640 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 641 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 642 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 643 | 644 | " size " + str(size_new) + " b " + 645 | "-" + str(size_change) + " b " + 646 | perc_change + " %") 647 | 648 | best_dict['chance_cutoff'] = chance_cutoff 649 | best_dict['size'] = size_new 650 | best_dict['count'] = count 651 | failed_attempts = 0 652 | arr_index = 0 653 | else: 654 | failed_attempts += 1 655 | if (failed_attempts >= max_attempts): 656 | break; 657 | 658 | 659 | 660 | 661 | 662 | def crush_chance_alpha(): # -Z 663 | # globals 664 | global best_dict 665 | global count 666 | global arr_index 667 | global output_best 668 | 669 | #locals 670 | range_chance_alpha = 128 671 | failed_attempts = 0 672 | max_attempts=200 673 | 674 | for chance_alpha in range(2, range_chance_alpha): 675 | count += 1 676 | 677 | raw_command = [ 678 | flif_binary, 679 | flif_to_flif, 680 | 681 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 682 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 683 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 684 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 685 | 686 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 687 | ('--chance-alpha=' + str(chance_alpha)), # <- 688 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 689 | ('--guess=' + str(best_dict['guess'])), 690 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 691 | 692 | best_dict['interlace'].flag, 693 | best_dict['no_channel_compact'].flag, 694 | best_dict['force_color_buckets'].flag, 695 | best_dict['no_ycocg'].flag, 696 | best_dict['keep_invisible_rgb'].flag, 697 | best_dict['no_subtract_green'].flag, 698 | 699 | INFILE, 700 | interlace_flag, 701 | '--overwrite', 702 | '/dev/stdout', 703 | ] # = raw_command 704 | 705 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 706 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 707 | size_new = sys.getsizeof(output) 708 | showActivity("chance alpha: " + str(chance_alpha), size_new) 709 | 710 | 711 | #if (DEBUG): 712 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':maniac_min_size, 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 713 | 714 | 715 | if (best_dict['size'] > size_new): # new file is smaller 716 | failed_attempts = 0 # reset break-counter 717 | output_best = output 718 | size_change = best_dict['size']-size_new 719 | perc_change = pct_of_best(size_new) 720 | print("\033[K", end="") 721 | print( 722 | str(count) + 723 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 724 | " threshold: " + str(best_dict['maniac_threshold']) + 725 | " min_size: " + str(best_dict['maniac_min_size']) + 726 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 727 | 728 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 729 | " "+ TXT_UL + "alpha: " + str(chance_alpha) + TXT_RES + " ] " + # ] chance # <---. 730 | " palette: " + str(best_dict['max_palette_size']) + 731 | 732 | " itlc: " + str(best_dict['interlace'].bool) + 733 | " guess: " + str(best_dict['guess']) + 734 | " inv_guess: " + str(best_dict['invisible_guess']) + 735 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 736 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 737 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 738 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 739 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 740 | 741 | " size " + str(size_new) + " b " + 742 | "-" + str(size_change) + " b " + 743 | perc_change + " %") 744 | 745 | best_dict['chance_alpha'] = chance_alpha 746 | best_dict['size'] = size_new 747 | best_dict['count'] = count 748 | failed_attempts = 0 749 | arr_index = 0 750 | else: 751 | failed_attempts += 1 752 | if (failed_attempts >= max_attempts): 753 | break; 754 | 755 | 756 | def crush_palette(): 757 | # globals 758 | global best_dict 759 | global count 760 | global arr_index 761 | global output_best 762 | 763 | #locals 764 | failed_attempts = 0 765 | max_attempts=200 766 | 767 | range1 = range(-11, 11) 768 | range2 = range(inf['colors']-5, inf['colors']+10) 769 | range3 = range(-inf['colors'], 10-inf['colors']) # negative 770 | max_palette_size_range = set(chain(range1, range2, range3)) 771 | 772 | for max_palette_size in max_palette_size_range: 773 | if ((max_palette_size < -32000) or (max_palette_size > 32000)) : # apparently negative values are ok 774 | continue 775 | 776 | count +=1 777 | 778 | raw_command = [ 779 | flif_binary, 780 | flif_to_flif, 781 | 782 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 783 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 784 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 785 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 786 | 787 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 788 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 789 | ('--max-palette-size=' + str(max_palette_size)), #<- 790 | ('--guess=' + str(best_dict['guess'])), 791 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 792 | 793 | best_dict['interlace'].flag, 794 | best_dict['no_channel_compact'].flag, 795 | best_dict['force_color_buckets'].flag, 796 | best_dict['no_ycocg'].flag, 797 | best_dict['keep_invisible_rgb'].flag, 798 | best_dict['no_subtract_green'].flag, 799 | 800 | INFILE, 801 | interlace_flag, 802 | '--overwrite', 803 | '/dev/stdout', 804 | ] # = raw_command 805 | 806 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 807 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 808 | size_new = sys.getsizeof(output) 809 | showActivity(("max palette size: " + str(max_palette_size)), size_new) 810 | 811 | 812 | #if (DEBUG): 813 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 814 | 815 | 816 | # crappy fix for #15 ; don't compare against size_orig at beginning 817 | smaller_than_global = (best_dict['size'] > size_new) 818 | smaller_than_flif_internal = (best_dict['size_flif_internal'] > size_new) 819 | 820 | # do the smaller_than_flif_internal check is needed because palette pass is as first pass 821 | # so we have to ignore it when we passed the initial iteration through all passes 822 | if (smaller_than_global) or ((it==0) and smaller_than_flif_internal): # new file is smaller 823 | failed_attempts = 0 # reset break-counter 824 | output_best = output 825 | if (smaller_than_global): 826 | size_change = best_dict['size']-size_new 827 | perc_change = pct_of_best(size_new) 828 | print("\033[K", end="") 829 | print( 830 | str(count) + 831 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 832 | " threshold: " + str(best_dict['maniac_threshold']) + 833 | " min_size: " + str(best_dict['maniac_min_size']) + 834 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 835 | 836 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 837 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 838 | " "+ TXT_UL + "palette: " + str(max_palette_size) + TXT_RES + # <---. 839 | 840 | " itlc: " + str(best_dict['interlace'].bool) + 841 | " guess: " + str(best_dict['guess']) + 842 | " inv_guess: " + str(best_dict['invisible_guess']) + 843 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 844 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 845 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 846 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 847 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 848 | 849 | " size " + str(size_new) + " b " + 850 | "-" + str(size_change) + " b " + 851 | perc_change + " %") 852 | if (size_new < best_dict['size_flif_internal']): 853 | best_dict['size_flif_internal'] = size_new 854 | best_dict['max_palette_size'] = max_palette_size 855 | best_dict['size'] = size_new 856 | best_dict['count'] = count 857 | failed_attempts = 0 858 | arr_index = 0 859 | else: 860 | failed_attempts += 1 861 | if (failed_attempts >= max_attempts): 862 | break; 863 | 864 | 865 | 866 | 867 | 868 | def crush_keep_invisible_rgb(): 869 | # globals 870 | global best_dict 871 | global count 872 | global arr_index 873 | global output_best 874 | 875 | for keep_invisible_rgb in True, False: 876 | count +=1 877 | 878 | flagstr = ("--keep-invisible-rgb" if (keep_invisible_rgb) else "") 879 | 880 | raw_command = [ 881 | flif_binary, 882 | flif_to_flif, 883 | 884 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 885 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 886 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 887 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 888 | 889 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 890 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 891 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 892 | ('--guess=' + str(best_dict['guess'])), 893 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 894 | 895 | best_dict['interlace'].flag, 896 | best_dict['no_channel_compact'].flag, 897 | best_dict['force_color_buckets'].flag, 898 | best_dict['no_ycocg'].flag, 899 | flagstr, 900 | best_dict['no_subtract_green'].flag, 901 | 902 | INFILE, 903 | interlace_flag, 904 | '--overwrite', 905 | '/dev/stdout', 906 | ] # = raw_command 907 | 908 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 909 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 910 | size_new = sys.getsizeof(output) 911 | showActivity("keep invisibler rgb: " + str(keep_invisible_rgb), size_new) 912 | 913 | 914 | #if (DEBUG): 915 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 916 | 917 | if (best_dict['size'] > size_new): # new file is smaller 918 | output_best = output 919 | size_change = best_dict['size']-size_new 920 | perc_change = pct_of_best(size_new) 921 | 922 | best_dict['keep_invisible_rgb'] = best_dict['keep_invisible_rgb']._replace(flag=flagstr) 923 | best_dict['keep_invisible_rgb'] = best_dict['keep_invisible_rgb']._replace(bool=keep_invisible_rgb) 924 | 925 | print("\033[K", end="") 926 | print( 927 | str(count) + 928 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 929 | " threshold: " + str(best_dict['maniac_threshold']) + 930 | " min_size: " + str(best_dict['maniac_min_size']) + 931 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 932 | 933 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 934 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 935 | " palette: " + str(best_dict['max_palette_size']) + 936 | 937 | " itlc: " + str(best_dict['interlace'].bool) + 938 | " guess: " + str(best_dict['guess']) + 939 | " inv_guess: " + str(best_dict['invisible_guess']) + 940 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 941 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 942 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 943 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 944 | " " + TXT_UL + "inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + TXT_RES + # < ------ 945 | 946 | " size " + str(size_new) + " b " + 947 | "-" + str(size_change) + " b " + 948 | perc_change + " %") 949 | 950 | best_dict['size'] = size_new 951 | best_dict['count'] = count 952 | arr_index = 0 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | def crush_force_color_buckets(): 965 | # globals 966 | global best_dict 967 | global count 968 | global arr_index 969 | global output_best 970 | 971 | for force_color_buckets in True, False: 972 | count +=1 973 | 974 | flagstr = ("--force-color-buckets" if (force_color_buckets) else "--no-color-buckets") 975 | 976 | raw_command = [ 977 | flif_binary, 978 | flif_to_flif, 979 | 980 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 981 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 982 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 983 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 984 | 985 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 986 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 987 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 988 | ('--guess=' + str(best_dict['guess'])), 989 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 990 | 991 | best_dict['interlace'].flag, 992 | best_dict['no_channel_compact'].flag, 993 | best_dict['force_color_buckets'].flag, 994 | flagstr, 995 | best_dict['keep_invisible_rgb'].flag, 996 | best_dict['no_subtract_green'].flag, 997 | 998 | INFILE, 999 | interlace_flag, 1000 | '--overwrite', 1001 | '/dev/stdout', 1002 | ] # = raw_command 1003 | 1004 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 1005 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 1006 | size_new = sys.getsizeof(output) 1007 | showActivity("force color buckets: " + str(force_color_buckets), size_new) 1008 | 1009 | 1010 | #if (DEBUG): 1011 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 1012 | 1013 | if (best_dict['size'] > size_new): # new file is smaller 1014 | output_best = output 1015 | size_change = best_dict['size']-size_new 1016 | perc_change = pct_of_best(size_new) 1017 | 1018 | best_dict['force_color_buckets'] = best_dict['force_color_buckets']._replace(flag=flagstr) 1019 | best_dict['force_color_buckets'] = best_dict['force_color_buckets']._replace(bool=force_color_buckets) 1020 | 1021 | print("\033[K", end="") 1022 | print( 1023 | str(count) + 1024 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 1025 | " threshold: " + str(best_dict['maniac_threshold']) + 1026 | " min_size: " + str(best_dict['maniac_min_size']) + 1027 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 1028 | 1029 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 1030 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 1031 | " palette: " + str(best_dict['max_palette_size']) + 1032 | 1033 | " itlc: " + str(best_dict['interlace'].bool) + 1034 | " guess: " + str(best_dict['guess']) + 1035 | " inv_guess: " + str(best_dict['invisible_guess']) + 1036 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 1037 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 1038 | " " + TXT_UL + "Cbuck: " + str(best_dict['force_color_buckets'].bool) + TXT_RES + # <- 1039 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 1040 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 1041 | 1042 | " size " + str(size_new) + " b " + 1043 | "-" + str(size_change) + " b " + 1044 | perc_change + " %") 1045 | 1046 | best_dict['size'] = size_new 1047 | best_dict['count'] = count 1048 | arr_index = 0 1049 | 1050 | 1051 | 1052 | def crush_no_ycocg(): 1053 | # globals 1054 | global best_dict 1055 | global count 1056 | global arr_index 1057 | global output_best 1058 | 1059 | for force_no_ycocg in True, False: 1060 | count +=1 1061 | 1062 | flagstr = ("--no-ycocg" if (force_no_ycocg) else "") 1063 | 1064 | raw_command = [ 1065 | flif_binary, 1066 | flif_to_flif, 1067 | 1068 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 1069 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 1070 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 1071 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 1072 | 1073 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 1074 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 1075 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 1076 | ('--guess=' + str(best_dict['guess'])), 1077 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 1078 | 1079 | 1080 | best_dict['interlace'].flag, 1081 | best_dict['no_channel_compact'].flag, 1082 | best_dict['force_color_buckets'].flag, 1083 | flagstr, 1084 | best_dict['keep_invisible_rgb'].flag, 1085 | best_dict['no_subtract_green'].flag, 1086 | 1087 | INFILE, 1088 | interlace_flag, 1089 | '--overwrite', 1090 | '/dev/stdout', 1091 | ] # = raw_command 1092 | 1093 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 1094 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 1095 | size_new = sys.getsizeof(output) 1096 | showActivity("no ycocg " + str(force_no_ycocg), size_new) 1097 | 1098 | 1099 | #if (DEBUG): 1100 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 1101 | 1102 | if (best_dict['size'] > size_new): # new file is smaller 1103 | output_best = output 1104 | size_change = best_dict['size']-size_new 1105 | perc_change = pct_of_best(size_new) 1106 | 1107 | best_dict['no_ycocg'] = best_dict['no_ycocg']._replace(flag=flagstr) 1108 | best_dict['no_ycocg'] = best_dict['no_ycocg']._replace(bool=force_no_ycocg) 1109 | 1110 | print("\033[K", end="") 1111 | print( 1112 | str(count) + 1113 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 1114 | " threshold: " + str(best_dict['maniac_threshold']) + 1115 | " min_size: " + str(best_dict['maniac_min_size']) + 1116 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 1117 | 1118 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 1119 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 1120 | " palette: " + str(best_dict['max_palette_size']) + 1121 | 1122 | " itlc: " + str(best_dict['interlace'].bool) + 1123 | " guess: " + str(best_dict['guess']) + 1124 | " inv_guess: " + str(best_dict['invisible_guess']) + 1125 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 1126 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 1127 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 1128 | " " + TXT_UL + "no_ycocg: " + str(best_dict['no_ycocg'].bool) + TXT_RES + # <- 1129 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 1130 | 1131 | " size " + str(size_new) + " b " + 1132 | "-" + str(size_change) + " b " + 1133 | perc_change + " %") 1134 | 1135 | best_dict['size'] = size_new 1136 | best_dict['count'] = count 1137 | arr_index = 0 1138 | 1139 | def crush_no_channel_compact(): 1140 | # globals 1141 | global best_dict 1142 | global count 1143 | global arr_index 1144 | global output_best 1145 | 1146 | for no_channel_compact in True, False: 1147 | count +=1 1148 | 1149 | flagstr = ("--no-channel-compact" if (no_channel_compact) else "") 1150 | 1151 | raw_command = [ 1152 | flif_binary, 1153 | flif_to_flif, 1154 | 1155 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 1156 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 1157 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 1158 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 1159 | 1160 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 1161 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 1162 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 1163 | ('--guess=' + str(best_dict['guess'])), 1164 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 1165 | 1166 | best_dict['interlace'].flag, 1167 | flagstr, 1168 | best_dict['force_color_buckets'].flag, 1169 | best_dict['no_ycocg'].flag, 1170 | best_dict['keep_invisible_rgb'].flag, 1171 | best_dict['no_subtract_green'].flag, 1172 | 1173 | INFILE, 1174 | interlace_flag, 1175 | '--overwrite', 1176 | '/dev/stdout', 1177 | ] # = raw_command 1178 | 1179 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 1180 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 1181 | size_new = sys.getsizeof(output) 1182 | showActivity("no channel compact: " + str(no_channel_compact), size_new) 1183 | 1184 | 1185 | #if (DEBUG): 1186 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 1187 | 1188 | if (best_dict['size'] > size_new): # new file is smaller 1189 | output_best = output 1190 | size_change = best_dict['size']-size_new 1191 | perc_change = pct_of_best(size_new) 1192 | 1193 | best_dict['no_channel_compact'] = best_dict['no_channel_compact']._replace(flag=flagstr) 1194 | best_dict['no_channel_compact'] = best_dict['no_channel_compact']._replace(bool=no_channel_compact) 1195 | 1196 | print("\033[K", end="") 1197 | print( 1198 | str(count) + 1199 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 1200 | " threshold: " + str(best_dict['maniac_threshold']) + 1201 | " min_size: " + str(best_dict['maniac_min_size']) + 1202 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 1203 | 1204 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 1205 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 1206 | " palette: " + str(best_dict['max_palette_size']) + 1207 | 1208 | " itlc: " + str(best_dict['interlace'].bool) + 1209 | " guess: " + str(best_dict['guess']) + 1210 | " inv_guess: " + str(best_dict['invisible_guess']) + 1211 | " " + TXT_UL + "no_CC: " + str(best_dict['no_channel_compact'].bool) + TXT_RES + # <- 1212 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 1213 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 1214 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 1215 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 1216 | 1217 | " size " + str(size_new) + " b " + 1218 | "-" + str(size_change) + " b " + 1219 | perc_change + " %") 1220 | 1221 | best_dict['size'] = size_new 1222 | best_dict['count'] = count 1223 | arr_index = 0 1224 | 1225 | 1226 | 1227 | 1228 | def crush_interlace(): 1229 | # globals 1230 | global best_dict 1231 | global count 1232 | global arr_index 1233 | global output_best 1234 | 1235 | for interlace in True, False: 1236 | guessline="" 1237 | interlace_flagstr = ("--interlace" if (interlace) else "--no-interlace") 1238 | if (interlace): 1239 | for a in 0,1,2,3: # --guess=abcd 1240 | for b in 0,1,2,3: 1241 | for c in 0,1,2,3: 1242 | for d in 0,1,2,3: 1243 | pixel_predictor = str(a) + str(b) + str(c) + str(d) 1244 | for invisible_guess in 0,1,2: # --invisible-guess={0,1,2} 1245 | invisible_guess_flagstr = ("--invisible-guess=" + str(invisible_guess)) 1246 | count += 1 1247 | 1248 | raw_command = [ 1249 | flif_binary, 1250 | flif_to_flif, 1251 | 1252 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 1253 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 1254 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 1255 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 1256 | 1257 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 1258 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 1259 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 1260 | 1261 | ('--guess=' + pixel_predictor), 1262 | interlace_flagstr, 1263 | invisible_guess_flagstr, 1264 | best_dict['no_channel_compact'].flag, 1265 | best_dict['force_color_buckets'].flag, 1266 | best_dict['no_ycocg'].flag, 1267 | best_dict['keep_invisible_rgb'].flag, 1268 | best_dict['no_subtract_green'].flag, 1269 | 1270 | 1271 | INFILE, 1272 | '--overwrite', 1273 | '/dev/stdout', 1274 | ] # = raw_command 1275 | 1276 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 1277 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 1278 | size_new = sys.getsizeof(output) 1279 | showActivity("interlace: " + (str(interlace) + " guess: " + str(pixel_predictor)) + " inv_guess: " + str(invisible_guess) , size_new) 1280 | 1281 | 1282 | 1283 | #if (DEBUG): 1284 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 1285 | 1286 | if (best_dict['size'] > size_new): # new file is smaller 1287 | 1288 | output_best = output 1289 | size_change = best_dict['size']-size_new 1290 | perc_change = pct_of_best(size_new) 1291 | 1292 | best_dict['interlace'] = best_dict['interlace']._replace(flag=interlace_flagstr) 1293 | best_dict['interlace'] = best_dict['interlace']._replace(bool=interlace) 1294 | best_dict['invisible_guess'] = invisible_guess 1295 | best_dict['guess'] = pixel_predictor 1296 | 1297 | print("\033[K", end="") 1298 | print( 1299 | str(count) + 1300 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 1301 | " threshold: " + str(best_dict['maniac_threshold']) + 1302 | " min_size: " + str(best_dict['maniac_min_size']) + 1303 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 1304 | 1305 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 1306 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 1307 | " palette: " + str(best_dict['max_palette_size']) + 1308 | 1309 | " " + TXT_UL + "itlc: " + str(best_dict['interlace'].bool) + # <--- 1310 | " guess: " + str(best_dict['guess']) + # <--- 1311 | " inv_Guess: " + str(best_dict['invisible_guess']) + TXT_RES + # <--- 1312 | " no_CC: " + str(best_dict['no_channel_compact'].bool) + 1313 | " no_subG: " + str(best_dict['no_subtract_green'].bool) + 1314 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 1315 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 1316 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 1317 | 1318 | " size " + str(size_new) + " b " + 1319 | "-" + str(size_change) + " b " + 1320 | str(perc_change) + " %") 1321 | 1322 | best_dict['size'] = size_new 1323 | best_dict['count'] = count 1324 | arr_index = 0 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | def crush_no_subtract_green(): 1334 | # globals 1335 | global best_dict 1336 | global count 1337 | global arr_index 1338 | global output_best 1339 | 1340 | for no_subtract_green in True, False: 1341 | count +=1 1342 | 1343 | flagstr = ("--no-subtract-green" if (no_subtract_green) else "") 1344 | 1345 | raw_command = [ 1346 | flif_binary, 1347 | flif_to_flif, 1348 | 1349 | ('--maniac-repeats=' + str(best_dict['maniac_repeats'])), 1350 | ('--maniac-threshold=' + str(best_dict['maniac_threshold'])), 1351 | ('--maniac-divisor=' + str(best_dict['maniac_divisor'])), 1352 | ('--maniac-min-size=' + str(best_dict['maniac_min_size'])), 1353 | 1354 | ('--chance-cutoff=' + str(best_dict['chance_cutoff'])), 1355 | ('--chance-alpha=' + str(best_dict['chance_alpha'])), 1356 | ('--max-palette-size=' + str(best_dict['max_palette_size'])), 1357 | ('--guess=' + str(best_dict['guess'])), 1358 | ('--invisible-guess=' + str(best_dict['invisible_guess'])), 1359 | 1360 | best_dict['interlace'].flag, 1361 | best_dict['no_channel_compact'].flag, 1362 | best_dict['force_color_buckets'].flag, 1363 | best_dict['no_ycocg'].flag, 1364 | best_dict['keep_invisible_rgb'].flag, 1365 | flagstr, 1366 | 1367 | INFILE, 1368 | interlace_flag, 1369 | '--overwrite', 1370 | '/dev/stdout', 1371 | ] # = raw_command 1372 | 1373 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 1374 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 1375 | size_new = sys.getsizeof(output) 1376 | showActivity("no subtract green: " + str(no_subtract_green), size_new) 1377 | 1378 | 1379 | #if (DEBUG): 1380 | # debug_array.append([{'Nr':count, 'maniac_repeats':str(best_dict['maniac_repeats']), 'maniac_threshold':str(best_dict['maniac_threshold']), 'maniac_min_size':str(best_dict['maniac_min_size']), 'maniac_divisor':str(best_dict['maniac_divisor']), 'max_palette_size':max_palette_size, 'ACB':ACB, 'INT': INTERLACE, 'size': size_new}]) 1381 | 1382 | if (best_dict['size'] > size_new): # new file is smaller 1383 | output_best = output 1384 | size_change = best_dict['size']-size_new 1385 | perc_change = pct_of_best(size_new) 1386 | 1387 | best_dict['no_subtract_green'] = best_dict['no_subtract_green']._replace(flag=flagstr) 1388 | best_dict['no_subtract_green'] = best_dict['no_subtract_green']._replace(bool=no_subtract_green) 1389 | 1390 | print("\033[K", end="") 1391 | print( 1392 | str(count) + 1393 | " maniac [ repeat: " + str(best_dict['maniac_repeats']) + 1394 | " threshold: " + str(best_dict['maniac_threshold']) + 1395 | " min_size: " + str(best_dict['maniac_min_size']) + 1396 | " divisor: " + str(best_dict['maniac_divisor']) + " ] " + # ] maniac 1397 | 1398 | " chance:[ cutoff: " + str(best_dict['chance_cutoff']) + 1399 | " alpha: " + str(best_dict['chance_alpha']) + " ] " + # ] chance 1400 | " palette: " + str(best_dict['max_palette_size']) + 1401 | 1402 | " itlc: " + str(best_dict['interlace'].bool) + 1403 | " guess: " + str(best_dict['guess']) + 1404 | " inv_guess: " + str(best_dict['invisible_guess']) + 1405 | "no_CC: " + str(best_dict['no_channel_compact'].bool) + 1406 | " " + TXT_UL + "no_subG: " + str(best_dict['no_subtract_green'].bool) + TXT_RES + # <- 1407 | " Cbuck: " + str(best_dict['force_color_buckets'].bool) + 1408 | " no_ycocg: " + str(best_dict['no_ycocg'].bool) + 1409 | " inv_rgb: " + str(best_dict['keep_invisible_rgb'].bool) + 1410 | 1411 | " size " + str(size_new) + " b " + 1412 | "-" + str(size_change) + " b " + 1413 | perc_change + " %") 1414 | 1415 | best_dict['size'] = size_new 1416 | best_dict['count'] = count 1417 | arr_index = 0 1418 | 1419 | 1420 | 1421 | 1422 | 1423 | 1424 | 1425 | 1426 | # make sure we know where flif binary is 1427 | flif_binary = "" 1428 | try: # search for "FLIF" enviromental variable first 1429 | flif_path = os.environ['FLIF'] 1430 | if os.path.isfile(flif_path): # the variable actually points to a file 1431 | flif_binary = flif_path 1432 | except KeyError: # env var not set, check if /usr/bin/flif exists 1433 | if (flif_binary == ""): 1434 | if (os.path.isfile("/usr/bin/flif")): 1435 | flif_binary = "/usr/bin/flif" 1436 | elif (os.path.isfile("/usr/share/bin/flif")): 1437 | flif_binary = "/usr/share/bin/flif" 1438 | else: 1439 | print("Error: no flif binary found, please use 'export FLIF=/path/to/flif'") 1440 | os.exit(1) 1441 | 1442 | 1443 | SUPPORTED_FILE_EXTENSIONS=['png', 'flif'] # @TODO add some more 1444 | input_files = [] 1445 | bad_input_files = [] 1446 | try: # catch KeyboardInterrupt 1447 | 1448 | for path in INPATHS: # iterate over arguments 1449 | if (os.path.isfile(path)): # inpath is a file 1450 | input_files.append(path) # add to list 1451 | elif (os.path.isdir(path)): # inpath is a directory 1452 | for root, directories, filenames in os.walk(path): 1453 | for filename in filenames: 1454 | if (filename.split('.')[-1] in SUPPORTED_FILE_EXTENSIONS): # check for valid filetypes 1455 | input_files.append(os.path.join(root,filename)) # add to list 1456 | else: # path does not exist 1457 | bad_input_files.append(path) 1458 | 1459 | if (bad_input_files): 1460 | print("WARNING: files not found:") 1461 | print(', '.join(bad_input_files) + "\n") 1462 | 1463 | # generation of input_files list is done: 1464 | 1465 | current_file = 0 1466 | for INFILE in input_files: # iterate over every file that we go 1467 | current_file += 1 1468 | file_count_str = "(" + str(current_file) + "/" + str(len(input_files)) + ") " # X/Yth file 1469 | flif_to_flif = "" 1470 | files_count_glob += 1 1471 | #output some metrics about the png that we are about to convert 1472 | inf={'path': INFILE, 'sizeByte': 0, 'colors': 0, 'sizeX': 0, 'sizeY':0, 'px':0, 'filetype': INFILE.split('.')[-1]} 1473 | 1474 | if (inf['filetype'] == "flif"): # PIL does not know flif (yet...?), so we convert the .flif to .png, catch it, and get the amount of pixels 1475 | flif_to_flif = "-t" # flif needs -t flag in case of flif to flif 1476 | FIFO=get_rand_filename() + ".png" # make sure path does not exist before 1477 | os.mkfifo(FIFO) # create named pipe 1478 | subprocess.Popen([flif_binary, INFILE, FIFO]) # convert flif to png to get pixel data 1479 | im = Image.open(FIFO) # <- png data 1480 | os.remove(FIFO) # remove named pipe 1481 | else: 1482 | im = Image.open(INFILE) 1483 | 1484 | # @TODO: can we speed this up? 1485 | # just for fun: 1486 | img=[] # will contain px data 1487 | for px in (im.getdata()): # iterate over the pixels of the input image so we can count the number of different colors 1488 | img.append(px) 1489 | 1490 | inf={'path': INFILE, 'sizeByte': os.path.getsize(INFILE), 'colors': len(Counter(img).items()), 'sizeX': im.size[0], 'sizeY': im.size[1], 'px': im.size[0]*im.size[1], 'filetype': INFILE.split('.')[-1]} 1491 | 1492 | print(file_count_str + inf['path'] + "; dimensions: " + str(inf['sizeX']) +"×"+ str(inf['sizeY']) + ", " + str(inf['sizeX']*inf['sizeY']) + " px, " + str(inf['colors']) + " unique colors," + " " + str(inf['sizeByte']) + " b") 1493 | size_orig = inf['sizeByte'] 1494 | size_before_glob += size_orig 1495 | 1496 | 1497 | # use named tuples for boolean cmdline flags 1498 | Boolflag = namedtuple('boolflag', 'flag bool') # define the structure 1499 | 1500 | global best_dict 1501 | # these have to be the flif default values 1502 | # basically we want to cover all non-lossy flif cmdline encode options here 1503 | best_dict={'count': -1, 1504 | 'maniac_repeats': 0, # 3 1505 | 'maniac_threshold': 40, 1506 | 'maniac_min_size': 50, 1507 | 'maniac_divisor': 30, 1508 | 'max_palette_size': 1024, 1509 | 'chance_cutoff': 2, 1510 | 'chance_alpha': 19, 1511 | 'interlace': Boolflag("--no-interlace", False), 1512 | 'guess': '0000', # may need further changes 1513 | 'invisible_guess': '0', 1514 | 'no_channel_compact': Boolflag('--no-channel-compact', True), 1515 | 'force_color_buckets': Boolflag('', False), #--force-color-buckets 1516 | 'no_ycocg': Boolflag("", False), # --no-ycocg 1517 | 'keep_invisible_rgb': Boolflag("--keep-invisible-rgb", False), 1518 | 'no_subtract_green': Boolflag("--no-subtract-green", False), 1519 | 'size': size_orig, 1520 | 'size_flif_internal': float('inf'), 1521 | } 1522 | 1523 | 1524 | global count 1525 | count = 0 # how many recompression attempts did we take? 1526 | best_count = 0 # what was the smallest compression so far? 1527 | 1528 | size_new = size_best = os.path.getsize(INFILE) 1529 | 1530 | if (COMPARE): #do a default flif run with no special arguments 1531 | proc = subprocess.Popen([flif_binary, INFILE,'--overwrite', '/dev/stdout'], stdout=subprocess.PIPE) 1532 | output_flifdefault = proc.stdout.read() 1533 | size_flifdefault = sys.getsizeof(output_flifdefault) 1534 | size_flifdefault_glob += size_flifdefault 1535 | 1536 | if (DEBUG): 1537 | debug_array=[] 1538 | debug_dict = {'Nr': '', 'maniac_repeats':'', 'maniac_threshold':"", 'maniac_min_size':"", 'maniac_divisor':"", 'max_palette_size': "", 'ACB': "", 'INT':"", 'size':""} 1539 | 1540 | max_iterations = 10 1541 | it = 0 1542 | #best_dict['maniac_repeats'] = 1543 | last_changing_pass = 0 1544 | 1545 | while (it != max_iterations): 1546 | # note: if none of the passes manage to reduce, we will have an infinite loop here! 1547 | 1548 | size_before = best_dict['size'] 1549 | 1550 | if (last_changing_pass == 1): break 1551 | crush_maniac_threshold() # 1 1552 | if best_dict['size'] < size_before: 1553 | size_before = best_dict['size'] 1554 | last_changing_pass = 1 1555 | 1556 | if (last_changing_pass == 2): break 1557 | crush_maniac_divisor() # 2 1558 | if best_dict['size'] < size_before: 1559 | size_before = best_dict['size'] 1560 | last_changing_pass = 2 1561 | 1562 | 1563 | if (last_changing_pass == 3): break 1564 | crush_maniac_min_size() # 3 1565 | if best_dict['size'] < size_before: 1566 | size_before = best_dict['size'] 1567 | last_changing_pass = 3 1568 | 1569 | if (last_changing_pass == 4): break 1570 | crush_palette() # 4 1571 | if best_dict['size'] < size_before: 1572 | size_before = best_dict['size'] 1573 | last_changing_pass = 4 1574 | 1575 | 1576 | if (last_changing_pass == 5): break 1577 | crush_chance_cutoff() # 5 1578 | if best_dict['size'] < size_before: 1579 | size_before = best_dict['size'] 1580 | last_changing_pass = 5 1581 | 1582 | if (last_changing_pass == 6): break 1583 | crush_chance_alpha() # 6 1584 | if best_dict['size'] < size_before: 1585 | size_before = best_dict['size'] 1586 | last_changing_pass = 6 1587 | 1588 | if (last_changing_pass == 7): break 1589 | crush_keep_invisible_rgb() # 7 1590 | if best_dict['size'] < size_before: 1591 | size_before = best_dict['size'] 1592 | last_changing_pass = 7 1593 | 1594 | if (last_changing_pass == 8): break 1595 | crush_force_color_buckets() # 8 1596 | if best_dict['size'] < size_before: 1597 | size_before = best_dict['size'] 1598 | last_changing_pass = 8 1599 | 1600 | if (last_changing_pass == 9): break 1601 | crush_no_ycocg() # 9 1602 | if best_dict['size'] < size_before: 1603 | size_before = best_dict['size'] 1604 | last_changing_pass = 9 1605 | 1606 | if (last_changing_pass == 10): break 1607 | crush_no_subtract_green() # 10 1608 | if best_dict['size'] < size_before: 1609 | size_before = best_dict['size'] 1610 | last_changing_pass = 10 1611 | 1612 | if (last_changing_pass == 11): break 1613 | crush_no_channel_compact() # 11 1614 | if best_dict['size'] < size_before: 1615 | size_before = best_dict['size'] 1616 | last_changing_pass = 11 1617 | 1618 | if (last_changing_pass == 12): break 1619 | crush_interlace() # 12 1620 | if best_dict['size'] < size_before: 1621 | size_before = best_dict['size'] 1622 | last_changing_pass = 12 1623 | 1624 | if (last_changing_pass == 13): break 1625 | crush_maniac_repeats() # 13 1626 | if best_dict['size'] < size_before: 1627 | size_before = best_dict['size'] 1628 | last_changing_pass = 13 1629 | 1630 | it+=1 1631 | 1632 | 1633 | if (COMPARE): # how does flifcrush compare to default flif conversion? 1634 | diff_to_flif_byte = best_dict['size'] - size_flifdefault 1635 | if (best_dict['size'] > size_flifdefault): 1636 | print("WARNING: flifcrush failed reducing reducing size better than default flif, please report! File: " + INFILE) 1637 | diff_to_flif_perc = (((size_flifdefault-best_dict['size']) / best_dict['size'])*100) 1638 | print("\033[K", end="") # clear previous line 1639 | print("\nComparing flifcrush (" + str(best_dict['size']) +" b) to default flif (" + str(size_flifdefault) + " b): " + str(diff_to_flif_byte) + " b which are " + str(diff_to_flif_perc)[:6] + " %") 1640 | 1641 | # write final best file 1642 | save_file() 1643 | 1644 | if (DEBUG): 1645 | for index, val in enumerate(debug_array): 1646 | print("run:", val[0]['Nr'], " maniac_repeats:", val[0]['maniac_repeats']," maniac_threshold:", val[0]['maniac_threshold']," maniac_min_size:", val[0]['maniac_min_size']," maniac_split_divisor:", val[0]['maniac_split_divisor']," max_palette_size:", val[0]['max_palette_size'], "ACB", val[0]['ACB'],"INT", val[0]['INT'], " size:", val[0]['size'] ) 1647 | if (files_count_glob > 1): 1648 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 1649 | if (COMPARE): 1650 | print("Flif default would have been: " + str(size_flifdefault_glob) + " b") 1651 | print(" Compared to original size: " + str(((size_flifdefault_glob - size_before_glob)/size_before_glob)*100)[:6] + "%" ) 1652 | sign = "+" if (((size_flifdefault_glob - size_after_glob)/size_after_glob)*100 > 0) else "" 1653 | print(" Compared to crushed size: " + sign + str(((size_flifdefault_glob - size_after_glob)/size_after_glob)*100)[:6] + "%") 1654 | except KeyboardInterrupt: # ctrl+c 1655 | print("\033[K", end="") # clear previous line 1656 | print("\rTermination requested, saving best file so far...\n") 1657 | try: # double ctrl+c shall quit immediately 1658 | save_file() 1659 | if (files_count_glob > 1): 1660 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 1661 | if (COMPARE): 1662 | print("Flif default would have been: " + str(size_flifdefault_glob) + " b") 1663 | print(" Compared to original size: " + str(((size_flifdefault_glob - size_before_glob)/size_before_glob)*100)[:6] + "%" ) 1664 | sign = "+" if (((size_flifdefault_glob - size_after_glob)/size_after_glob)*100 > 0) else "" 1665 | print(" Compared to crushed size: " + sign + str(((size_flifdefault_glob - size_after_glob)/size_after_glob)*100)[:6] + "%") 1666 | except KeyboardInterrupt: # double ctrl+c 1667 | print("\033[K", end="") # clear previous line 1668 | print("Terminated by user.") 1669 | -------------------------------------------------------------------------------- /old_flifcrush: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # flifcrush - tries to reduce FLIF files in size 4 | # Copyright (C) 2016 Matthias Krüger, Jon Sneyers 5 | 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2, or (at your option) 9 | # any later version. 10 | 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 19 | 20 | 21 | import subprocess 22 | import sys 23 | import os 24 | from PIL import Image 25 | from collections import Counter 26 | from collections import namedtuple 27 | import argparse 28 | from itertools import chain # combine ranges 29 | 30 | import random # getrandomfilename 31 | import string # getrandomfilename 32 | __author__ = 'Matthias "matthiaskrgr" Krüger' 33 | 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument("inpath", help="file or path (recursively) to be converted to flif", metavar='N', nargs='+', type=str) 36 | parser.add_argument("-o", "--options", help="extra command line options (e.g. -o-N for non-interlaced)", metavar='OPTIONS', nargs=1, type=str) 37 | parser.add_argument("-c", "--compare", help="compare to default flif compression", action='store_true') 38 | args = parser.parse_args() 39 | 40 | COMPARE = (args.compare) 41 | INPATHS = args.inpath 42 | 43 | if args.options: 44 | OPTIONS = args.options[0].split() 45 | else: 46 | OPTIONS = [] 47 | 48 | # make these global to access them easily inside functions 49 | global size_before_glob, size_after_glob, files_count_glob, size_flifdefault_glob 50 | size_before_glob = 0 # size of all images we process 51 | size_after_glob = 0 # size of all flifs we generated 52 | files_count_glob = 0 # number of files 53 | size_flifdefault_glob = 0 # size of all images converted with flif default parameters 54 | 55 | 56 | # colors for stdout 57 | txt_ul = TXT_UL = '\033[04m' # underline 58 | txt_res = TXT_RES = '\033[0m' #reset 59 | 60 | 61 | #BRUTEFORCE = (args.bruteforce) 62 | global output_best 63 | output_best="none" 64 | global arr_index 65 | global progress_array 66 | arr_index = 0 67 | #progress_array=["|", "/", "-", "\\",] 68 | #progress_array=[".", "o", "0", "O", "O", "o", "."] 69 | progress_array=[" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"] 70 | arrlen=len(progress_array) 71 | 72 | # prints activity indicator (some kind of ascii 'animation') 73 | def showActivity(func_arg, size_new): 74 | global arr_index 75 | arr_index+=1 76 | if (arr_index == arrlen): 77 | arr_index = 0 78 | diff_best = best_dict['size'] - size_new 79 | sys.stderr.write(progress_array[arr_index] + " " + str(count) + ": " + str(func_arg) + ", size: " + str(size_new) + " b \r") 80 | 81 | # save .flif file that had the best combination of parameters 82 | def save_file(): 83 | global output_best 84 | flif2flif = False # default, we need extra parameter if we convert .flif to -clif 85 | # if the condition is false, we didn't manage to reduce size 86 | if output_best != "none": 87 | OUTFILE=".".join(INFILE.split(".")[:-1])+".flif" # split by ".", rm last elm, join by "." and add "flif" extension 88 | 89 | if (OUTFILE == INFILE): # most likely flif fo flif crushing 90 | flif2flif = True 91 | OUTFILE=get_rand_filename() 92 | 93 | with open(OUTFILE, "w+b") as f: 94 | f.write(output_best) 95 | f.close 96 | 97 | size_flif=os.path.getsize(OUTFILE) 98 | size_orig=os.path.getsize(INFILE) 99 | 100 | if (flif2flif): # overwrite INFILE with OUTFILE 101 | os.remove(INFILE) 102 | os.rename(OUTFILE, INFILE) # rename outfile to infile 103 | 104 | # print some numbers 105 | global size_after_glob 106 | size_after_glob += size_flif 107 | size_diff = size_orig - best_dict['size'] 108 | 109 | print("\033[K", end="") 110 | print("reduced from " + str(size_orig) + " b to "+ str(best_dict['size']) + " ( -"+ str(size_diff) + " b, "+ str((( best_dict['size'] - size_orig)/ size_orig )*100)[:6] + " %) " + str(count) + " flif calls.\n\n") 111 | #print("reduced from {size_orig}b to {size_flif}b ({size_diff}b, {perc_change} %) via \n [{bestoptim}] and {cnt} flif calls.\n\n".format(size_orig = os.path.getsize(INFILE), size_flif=size_flif, size_diff=(size_flif - size_orig), perc_change=str(((size_flif-size_orig) / size_orig)*100)[:6], bestoptim=str("maniac repeats:" + str(best_dict['maniac_repeats']) + " maniac_threshold:" + str(best_dict['maniac_threshold']) + " maniac_min_size:" + str(best_dict['maniac_min_size'])+ " maniac_divisor:" + str(best_dict['maniac_divisor']) + " max_palette_size:" + str(best_dict['max_palette_size']) + " chance-cutoff:" + str(best_dict['chance_cutoff']) + " chance-alpha:" + str(best_dict['chance_alpha']) + " ACB:" + str(best_dict['ACB']) + " INTERLACE:" + str(best_dict['INT']) + " PLC:" + str(best_dict['PLC']) + " RGB:" + str(best_dict['RGB']) + " A:" + str(best_dict['A'])), cnt=str(count)), end="\r") 112 | 113 | if (best_dict['size'] > size_orig): 114 | print("WARNING: failed to reduce size") 115 | 116 | else: 117 | print("\033[K", end="") 118 | print("WARNING: could not reduce size!") 119 | 120 | # generates a name for a file that does not exist in current directory, used for tmp files 121 | def get_rand_filename(): 122 | # this prevents accidentally overwriting a preexisting file 123 | filename =''.join(random.choice(string.ascii_uppercase) for i in range(9)) 124 | while (os.path.isfile(filename)): # if the name already exists, try again 125 | filename =''.join(random.choice(string.ascii_uppercase) for i in range(9)) 126 | return filename 127 | 128 | def pct_of_best(size_new): 129 | # if best size was 100 and new file is 50, return 50 % 130 | pct = str(((size_new - best_dict['size']) / best_dict['size'])*100) 131 | pct = "-0.000" if ("e" in pct) else pct[:6] # due to too-early [:6], '8.509566454608271e-07' would become "8.509" 132 | return pct 133 | 134 | def range_around_default(default, min, max, stepsize): 135 | result = [] 136 | i = default 137 | plus = default+stepsize 138 | minus = default-stepsize 139 | c = 0 140 | while plus<=max or minus>=min: 141 | if plus<=max: 142 | result += [plus] 143 | plus += stepsize 144 | if minus>=min: 145 | result += [minus] 146 | minus -= stepsize 147 | c += 1 148 | if c>20: # list is getting long, increase stepsize to reach boundaries faster 149 | stepsize += 1 150 | c = 0 151 | return result 152 | 153 | def range_above_default(default, min, max, stepsize): 154 | result = [] 155 | plus = default+stepsize 156 | c = 0 157 | while plus<=max: 158 | result += [plus] 159 | plus += stepsize 160 | c += 1 161 | if c>10: # list is getting long, increase stepsize to reach boundaries faster 162 | stepsize += 1 163 | c = 0 164 | return result 165 | 166 | def range_below_default(default, min, max, stepsize): 167 | result = [] 168 | minus = default-stepsize 169 | c = 0 170 | while minus>=min: 171 | result += [minus] 172 | minus -= stepsize 173 | c += 1 174 | if c>10: # list is getting long, increase stepsize to reach boundaries faster 175 | stepsize += 1 176 | c = 0 177 | return result 178 | 179 | def strfield(highlight,parameters,field,shortname): 180 | if (field == highlight): 181 | return TXT_UL + shortname + str(parameters[field]) + TXT_RES 182 | else: 183 | return shortname + str(parameters[field]) 184 | 185 | def strfieldbool(highlight,parameters,field,shortname): 186 | if parameters[field]: 187 | if (field == highlight): 188 | return TXT_UL + ' ' + shortname + ' ' + TXT_RES 189 | else: 190 | return ' ' + shortname + ' ' 191 | else: 192 | if (field == highlight): 193 | return TXT_UL + '(' + shortname + ')' + TXT_RES 194 | else: 195 | return '(' + shortname + ')' 196 | 197 | def encode_attempt(field,value): 198 | # globals we modify 199 | global best_dict 200 | global count 201 | global arr_index 202 | global output_best 203 | 204 | count += 1 205 | parameters=best_dict.copy() 206 | parameters[field] = value 207 | raw_command = [ 208 | flif_binary, 209 | flif_to_flif, 210 | ('--maniac-repeats=' + str(parameters['maniac_repeats'])), 211 | ('--maniac-threshold=' + str(parameters['maniac_threshold'])), 212 | ('--maniac-divisor=' + str(parameters['maniac_divisor'])), 213 | ('--maniac-min-size=' + str(parameters['maniac_min_size'])), 214 | 215 | ('--chance-cutoff=' + str(parameters['chance_cutoff'])), 216 | ('--chance-alpha=' + str(parameters['chance_alpha'])), 217 | ('--max-palette-size=' + str(parameters['max_palette_size'])) 218 | ] 219 | if parameters['no_channel_compact']: 220 | raw_command += ['--no-channel-compact'] 221 | if str(parameters['force_color_buckets']) != '(-AB)': 222 | raw_command += [str(parameters['force_color_buckets'])] 223 | if parameters['no_ycocg']: 224 | raw_command += ['--no-ycocg'] 225 | if parameters['keep_invisible_rgb']: 226 | raw_command += ['--keep-invisible-rgb'] 227 | raw_command += [ 228 | INFILE, 229 | '/dev/stdout' 230 | ] # = raw_command 231 | raw_command += OPTIONS 232 | 233 | sanitized_command = [x for x in raw_command if x ] # remove empty elements, if any 234 | #print('running: ', sanitized_command) 235 | output = subprocess.Popen(sanitized_command, stdout=subprocess.PIPE).stdout.read() 236 | #size_new = sys.getsizeof(output) 237 | size_new = len(output) 238 | showActivity(field + ": " + str(value), size_new) 239 | if ((best_dict['size'] > size_new) or (count==1)): # new file is smaller // count==1: make sure best_dict is filled with first values we obtain. this way we still continue crushing even if initial N-run does not reduce size smaller than size_orig 240 | output_best = output 241 | if (size_orig > size_new): 242 | size_change = best_dict['size']-size_new 243 | perc_change = pct_of_best(size_new) 244 | print("\033[K", end="") 245 | print( 246 | str(count).ljust(4) + " | Options: " + 247 | 248 | # " maniac:[" + 249 | strfield(field,parameters,'maniac_repeats','-R') + ' ' + 250 | strfield(field,parameters,'maniac_threshold','-T') + ' ' + 251 | strfield(field,parameters,'maniac_min_size','-M') + ' ' + 252 | strfield(field,parameters,'maniac_divisor','-D') + 253 | ' ' + 254 | # "] " + # ] maniac 255 | 256 | # " chance:[" + 257 | strfield(field,parameters,'chance_cutoff','-X') + ' ' + 258 | strfield(field,parameters,'chance_alpha','-Z') + 259 | # "] " + # ] chance 260 | ' ' + 261 | 262 | strfield(field,parameters,'max_palette_size','-P') + ' ' + 263 | strfield(field,parameters,'force_color_buckets','') + ' ' + 264 | strfieldbool(field,parameters,'no_channel_compact','-C') + ' ' + 265 | strfieldbool(field,parameters,'keep_invisible_rgb','-K') + ' ' + 266 | strfieldbool(field,parameters,'no_ycocg','-Y') + ' ' + 267 | ' '.join(OPTIONS) + ' ' 268 | 269 | "\t| size " + str(size_new) + "b " + 270 | "-" + str(size_change) + "b " + 271 | perc_change + " %", flush=True) 272 | 273 | best_dict['size'] = size_new 274 | best_dict['count'] = count 275 | best_dict[field] = value 276 | arr_index = 0 277 | 278 | 279 | 280 | def crush_maniac_repeats(): # -R 281 | max_attempts = 3 282 | failed_attempts = 0 283 | for maniac_repeats in range_around_default(best_dict['maniac_repeats'],0,20,1): 284 | if encode_attempt('maniac_repeats',maniac_repeats): 285 | failed_attempts = 0 # reset break-counter 286 | else: # file is not smaller 287 | failed_attempts += 1 288 | if (failed_attempts >= max_attempts): 289 | break # break out of loop, we have wasted enough time here 290 | 291 | 292 | def crush_maniac_threshold(): # -T 293 | max_attempts = 20 294 | failed_attempts = 0 295 | improved = False 296 | for maniac_threshold in range_above_default(best_dict['maniac_threshold'],5,800,1): 297 | if encode_attempt('maniac_threshold',maniac_threshold): 298 | failed_attempts = 0 # reset break-counter 299 | improved = True 300 | else: # file is not smaller 301 | failed_attempts += 1 302 | if (failed_attempts >= max_attempts): 303 | break # break out of loop, we have wasted enough time here 304 | if improved: return # found something better, no need to check range_below_default 305 | failed_attempts = 0 306 | for maniac_threshold in range_below_default(best_dict['maniac_threshold'],5,800,1): 307 | if encode_attempt('maniac_threshold',maniac_threshold): 308 | failed_attempts = 0 # reset break-counter 309 | else: # file is not smaller 310 | failed_attempts += 1 311 | if (failed_attempts >= max_attempts): 312 | break # break out of loop, we have wasted enough time here 313 | 314 | 315 | def crush_maniac_divisor(): # -D 316 | max_attempts = 8 317 | failed_attempts = 0 318 | improved=False 319 | for maniac_divisor in range_above_default(best_dict['maniac_divisor'],1,100000,1): 320 | if encode_attempt('maniac_divisor',maniac_divisor): 321 | failed_attempts = 0 322 | improved=True 323 | else: 324 | failed_attempts += 1 325 | if (failed_attempts >= max_attempts): 326 | break 327 | if improved: return 328 | failed_attempts = 0 329 | for maniac_divisor in range_below_default(best_dict['maniac_divisor'],1,100000,1): 330 | if encode_attempt('maniac_divisor',maniac_divisor): 331 | failed_attempts = 0 332 | else: 333 | failed_attempts += 1 334 | if (failed_attempts >= max_attempts): 335 | break 336 | 337 | 338 | def crush_maniac_min_size(): # -M 339 | max_attempts = 5 340 | failed_attempts = 0 341 | improved = False 342 | for maniac_min_size in range_above_default(best_dict['maniac_min_size'],0,30000,5): 343 | if encode_attempt('maniac_min_size',maniac_min_size): 344 | failed_attempts = 0 345 | improved = True 346 | else: 347 | failed_attempts += 1 348 | if (failed_attempts >= max_attempts): 349 | break 350 | if improved: return 351 | failed_attempts = 0 352 | for maniac_min_size in range_below_default(best_dict['maniac_min_size'],0,30000,5): 353 | if encode_attempt('maniac_min_size',maniac_min_size): 354 | failed_attempts = 0 355 | else: 356 | failed_attempts += 1 357 | if (failed_attempts >= max_attempts): 358 | break 359 | 360 | def crush_chance_cutoff(): 361 | failed_attempts = 0 362 | max_attempts=5 363 | # should be possible to find the best cutoff quickly: 364 | # if optimal cutoff is higher than the default, compression should be increasingly better as the cutoff grows, 365 | # so no point to keep attempting higher and higher cutoffs 366 | 367 | for chance_cutoff in range_around_default(best_dict['chance_cutoff'],1,128,1): 368 | if encode_attempt('chance_cutoff',chance_cutoff): 369 | failed_attempts = 0 370 | else: 371 | failed_attempts += 1 372 | if (failed_attempts >= max_attempts): 373 | break 374 | 375 | 376 | def crush_chance_alpha(): # -Z 377 | failed_attempts = 0 378 | max_attempts=4 379 | improved=False 380 | for chance_alpha in range_above_default(best_dict['chance_alpha'],2,128,1): 381 | if encode_attempt('chance_alpha',chance_alpha): 382 | failed_attempts = 0 383 | improved = True 384 | else: 385 | failed_attempts += 1 386 | if (failed_attempts >= max_attempts): 387 | break 388 | if improved: return 389 | failed_attempts = 0 390 | for chance_alpha in range_below_default(best_dict['chance_alpha'],2,128,1): 391 | if encode_attempt('chance_alpha',chance_alpha): 392 | failed_attempts = 0 393 | else: 394 | failed_attempts += 1 395 | if (failed_attempts >= max_attempts): 396 | break 397 | 398 | def crush_max_palette_size(): 399 | for max_palette_size in [0, inf['colors']-1, inf['colors'], -inf['colors'], 1-inf['colors']]: 400 | if max_palette_size < 32000 and max_palette_size > -32000: 401 | encode_attempt('max_palette_size',max_palette_size) 402 | 403 | def crush_keep_invisible_rgb(): 404 | for keep_invisible_rgb in True, False: 405 | encode_attempt('keep_invisible_rgb',keep_invisible_rgb) 406 | 407 | def crush_force_color_buckets(): 408 | for force_color_buckets in '(-AB)','-A','-B': 409 | encode_attempt('force_color_buckets',force_color_buckets) 410 | 411 | def crush_no_ycocg(): 412 | for no_ycocg in True, False: 413 | encode_attempt('no_ycocg',no_ycocg) 414 | 415 | def crush_no_channel_compact(): 416 | for no_channel_compact in True, False: 417 | encode_attempt('no_channel_compact',no_channel_compact) 418 | 419 | 420 | 421 | # make sure we know where flif binary is 422 | flif_binary = "" 423 | try: # search for "FLIF" enviromental variable first 424 | flif_path = os.environ['FLIF'] 425 | if os.path.isfile(flif_path): # the variable actually points to a file 426 | flif_binary = flif_path 427 | except KeyError: # env var not set, check if /usr/bin/flif exists 428 | if (flif_binary == ""): 429 | if (os.path.isfile("/usr/bin/flif")): 430 | flif_binary = "/usr/bin/flif" 431 | elif (os.path.isfile("/usr/share/bin/flif")): 432 | flif_binary = "/usr/share/bin/flif" 433 | else: 434 | print("Error: no flif binary found, please use 'export FLIF=/path/to/flif'") 435 | os.exit(1) 436 | 437 | 438 | SUPPORTED_FILE_EXTENSIONS=['png', 'flif'] # @TODO add some more 439 | input_files = [] 440 | try: # catch KeyboardInterrupt 441 | 442 | for path in INPATHS: # iterate over arguments 443 | if (os.path.isfile(path)): # inpath is not a directory but a file 444 | input_files.append(path) # add to list 445 | else: # else walk recursively 446 | for root, directories, filenames in os.walk(path): 447 | for filename in filenames: 448 | if (filename.split('.')[-1] in SUPPORTED_FILE_EXTENSIONS): # check for valid filetypes 449 | input_files.append(os.path.join(root,filename)) # add to list 450 | 451 | # generation of input_files list is done: 452 | 453 | current_file = 0 454 | for INFILE in input_files: # iterate over every file that we go 455 | current_file += 1 456 | file_count_str = "(" + str(current_file) + "/" + str(len(input_files)) + ") " # X/Yth file 457 | flif_to_flif = "" 458 | files_count_glob += 1 459 | #output some metrics about the png that we are about to convert 460 | inf={'path': INFILE, 'sizeByte': 0, 'colors': 0, 'sizeX': 0, 'sizeY':0, 'px':0, 'filetype': INFILE.split('.')[-1]} 461 | 462 | if (inf['filetype'] == "flif"): # PIL does not know flif (yet...?), so we convert the .flif to .png, catch it, and get the amount of pixels 463 | flif_to_flif = "-t" # flif needs -t flag in case of flif to flif 464 | FIFO=get_rand_filename() + ".png" # make sure path does not exist before 465 | os.mkfifo(FIFO) # create named pipe 466 | subprocess.Popen([flif_binary, INFILE, FIFO]) # convert flif to png to get pixel data 467 | im = Image.open(FIFO) # <- png data 468 | os.remove(FIFO) # remove named pipe 469 | else: 470 | im = Image.open(INFILE) 471 | 472 | # @TODO: can we speed this up? 473 | # just for fun: 474 | img=[] # will contain px data 475 | for px in (im.getdata()): # iterate over the pixels of the input image so we can count the number of different colors 476 | img.append(px) 477 | 478 | inf={'path': INFILE, 'sizeByte': os.path.getsize(INFILE), 'colors': len(Counter(img).items()), 'sizeX': im.size[0], 'sizeY': im.size[1], 'px': im.size[0]*im.size[1], 'filetype': INFILE.split('.')[-1]} 479 | 480 | print(file_count_str + inf['path'] + "; dimensions: " + str(inf['sizeX']) +"×"+ str(inf['sizeY']) + ", " + str(inf['sizeX']*inf['sizeY']) + " px, " + str(inf['colors']) + " unique colors," + " " + str(inf['sizeByte']) + " b") 481 | size_orig = inf['sizeByte'] 482 | size_before_glob += size_orig 483 | 484 | 485 | global best_dict 486 | # these have to be the flif default values 487 | best_dict={'count': -1, 488 | 'maniac_repeats': 1, # 3 489 | 'maniac_threshold': 64, 490 | 'maniac_min_size': 50, 491 | 'maniac_divisor': 30, 492 | 'max_palette_size': 1024, 493 | 'chance_cutoff': 2, 494 | 'chance_alpha': 19, 495 | 'no_channel_compact': False, 496 | 'force_color_buckets': '(-AB)', # use the default encoder choice (which is neither -A nor -B) 497 | 'no_ycocg': False, 498 | 'keep_invisible_rgb': False, 499 | 'size': size_orig, 500 | } 501 | 502 | 503 | global count 504 | count = 0 # how many recompression attempts did we take? 505 | best_count = 0 # what was the smallest compression so far? 506 | 507 | size_new = size_best = os.path.getsize(INFILE) 508 | 509 | if (COMPARE): #do a default flif run with no special arguments except those provided 510 | proc = subprocess.Popen([flif_binary, INFILE, '/dev/stdout'] + OPTIONS, stdout=subprocess.PIPE) 511 | output_flifdefault = proc.stdout.read() 512 | #size_flifdefault = sys.getsizeof(output_flifdefault) 513 | size_flifdefault = len(output_flifdefault) 514 | size_flifdefault_glob += size_flifdefault 515 | 516 | max_iterations = 20 517 | it = 0 518 | 519 | # find the best color representation (should be mostly orthogonal to the other encode options) 520 | crush_max_palette_size() 521 | crush_force_color_buckets() 522 | crush_no_ycocg() 523 | crush_no_channel_compact() 524 | crush_keep_invisible_rgb() 525 | 526 | while (it < max_iterations): 527 | sze_beginning = best_dict['size'] 528 | # find the best chance cutoff 529 | crush_chance_cutoff() 530 | crush_chance_alpha() 531 | # try to find a good tree 532 | crush_maniac_threshold() 533 | crush_maniac_divisor() 534 | crush_maniac_min_size() 535 | crush_maniac_repeats() 536 | 537 | # if iteration didn't reduce anything, stop' 538 | it+=1 539 | if (sze_beginning == best_dict['size']): 540 | break 541 | 542 | 543 | if (COMPARE): # how does flifcrush compare to default flif conversion? 544 | diff_to_flif_byte = best_dict['size'] - size_flifdefault 545 | if (best_dict['size'] > size_flifdefault): 546 | print("WARNING: flifcrush failed reducing reducing size better than default flif, please report!") 547 | diff_to_flif_perc = (((size_flifdefault-best_dict['size']) / best_dict['size'])*100) 548 | print("\033[K", end="") # clear previous line 549 | print("\nComparing flifcrush (" + str(best_dict['size']) +" b) to default flif (" + str(size_flifdefault) + " b): " + str(diff_to_flif_byte) + " b which are " + str(diff_to_flif_perc)[:6] + " %") 550 | 551 | 552 | # write final best file 553 | save_file() 554 | 555 | if (files_count_glob > 1): 556 | if (COMPARE): 557 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 558 | print("Flif default would have been: " + str(size_flifdefault_glob) + " b") 559 | else: 560 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 561 | except KeyboardInterrupt: 562 | print("\033[K", end="") # clear previous line 563 | print("\rTermination requested, saving best file so far...\n") 564 | try: # double ctrl+c shall quit immediately 565 | save_file() 566 | if (files_count_glob > 1): 567 | if (COMPARE): 568 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 569 | print("Flif default would have been: " + str(size_flifdefault_glob) + " b") 570 | else: 571 | print("In total, reduced " + str(size_before_glob) + " b to " + str(size_after_glob) + " b, " + str(files_count_glob) + " files , " + str(((size_after_glob - size_before_glob)/size_before_glob)*100)[:6] + "%") 572 | except KeyboardInterrupt: # double ctrl+c 573 | print("\033[K", end="") # clear previous line 574 | print("Terminated by user.") 575 | 576 | 577 | 578 | -------------------------------------------------------------------------------- /samples/FreedroidRPG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FLIF-hub/flifcrush/3f47ce7b98fd0c6f9ed898e79fa8e8ef143c50ad/samples/FreedroidRPG.png -------------------------------------------------------------------------------- /samples/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FLIF-hub/flifcrush/3f47ce7b98fd0c6f9ed898e79fa8e8ef143c50ad/samples/screenshot.png --------------------------------------------------------------------------------