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