1#!/usr/bin/env python3 2# 3# Compress PNGs 4# 5# By Gerald Combs <gerald@wireshark.org 6# 7# SPDX-License-Identifier: GPL-2.0-or-later 8# 9'''Run various compression and optimization utilities on one or more PNGs''' 10 11import argparse 12import concurrent.futures 13import shutil 14import subprocess 15import sys 16 17PNG_FILE_ARG = '%PNG_FILE_ARG%' 18 19def get_compressors(): 20 # Add *lossless* compressors here. 21 compressors = { 22 # https://github.com/shssoichiro/oxipng 23 'oxipng': { 'args': ['--opt', 'max', '--strip', 'safe', PNG_FILE_ARG] }, 24 # http://optipng.sourceforge.net/ 25 'optipng': { 'args': ['-o3', '-quiet', PNG_FILE_ARG] }, 26 # https://github.com/amadvance/advancecomp 27 'advpng': { 'args': ['--recompress', '--shrink-insane', PNG_FILE_ARG] }, 28 # https://github.com/amadvance/advancecomp 29 'advdef': { 'args': ['--recompress', '--shrink-insane', PNG_FILE_ARG] }, 30 # https://pmt.sourceforge.io/pngcrush/ 31 'pngcrush': { 'args': ['-q', '-ow', '-brute', '-reduce', '-noforce', PNG_FILE_ARG, 'pngcrush.$$$$.png'] }, 32 # https://github.com/fhanau/Efficient-Compression-Tool 33 'ect': { 'args': ['-5', '--mt-deflate', '--mt-file', '-strip', PNG_FILE_ARG]} 34 } 35 for compressor in compressors: 36 compressor_path = shutil.which(compressor) 37 if compressor_path: 38 compressors[compressor]['path'] = compressor_path 39 return compressors 40 41 42def compress_png(png_file, compressors): 43 for compressor in compressors: 44 if not compressors[compressor].get('path', False): 45 next 46 47 args = compressors[compressor]['args'] 48 args = [arg.replace(PNG_FILE_ARG, png_file) for arg in args] 49 50 try: 51 compress_proc = subprocess.run([compressor] + args) 52 except Exception: 53 print('{} returned {}:'.format(compressor, compress_proc.returncode)) 54 55 56def main(): 57 parser = argparse.ArgumentParser(description='Compress PNGs') 58 parser.add_argument('--list', action='store_true', 59 help='List available compressors') 60 parser.add_argument('png_files', nargs='+', metavar='png file', help='Files to compress') 61 args = parser.parse_args() 62 63 compressors = get_compressors() 64 65 c_count = 0 66 for compressor in compressors: 67 if 'path' in compressors[compressor]: 68 c_count += 1 69 70 if c_count < 1: 71 sys.stderr.write('No compressors found\n') 72 sys.exit(1) 73 74 if args.list: 75 for compressor in compressors: 76 path = compressors[compressor].get('path', 'Not found') 77 print('{}: {}'.format(compressor, path)) 78 sys.exit(0) 79 80 with concurrent.futures.ProcessPoolExecutor() as executor: 81 futures = [] 82 for png_file in args.png_files: 83 print('Compressing {}'.format(png_file)) 84 futures.append(executor.submit(compress_png, png_file, compressors)) 85 concurrent.futures.wait(futures) 86 87 88if __name__ == '__main__': 89 main() 90