Advertisement
andreymal

compress_images.py

Dec 30th, 2015
414
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.20 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3.  
  4. import os
  5. import sys
  6. import traceback
  7. from io import BytesIO
  8. from hashlib import sha256
  9.  
  10.  
  11. # copypasted from https://bitbucket.org/andreymal/smilepack/src/develop/smilepack/utils/uploader.py
  12.  
  13.  
  14. class BadImageError(Exception):
  15.     pass
  16.  
  17.  
  18. def calc_hashsum(data):
  19.     return sha256(data).hexdigest()
  20.  
  21.  
  22. def compress_image(data, hashsum, image=None, compress_size=None):
  23.     from PIL import Image
  24.  
  25.     min_size = len(data)
  26.  
  27.     # Если сжимать совсем нет смысла
  28.     if min_size <= 4096:
  29.         return data, hashsum, None
  30.  
  31.     image_local = not image
  32.     if image_local:
  33.         try:
  34.             image = Image.open(BytesIO(data))
  35.         except:
  36.             raise BadImageError('Cannot decode image')
  37.  
  38.     try:
  39.         # Если сжимать не умеем
  40.         if image.format != 'PNG':
  41.             return data, hashsum, None
  42.  
  43.         # А PNG пробуем сжать разными методами
  44.         test_data, method = compress_png(image)
  45.     finally:
  46.         if image_local:
  47.             image.close()
  48.             image = None
  49.  
  50.     # Сохраняем сжатие, только если оно существенно
  51.     if test_data and min_size - len(test_data) > 1024:
  52.         new_hashsum = calc_hashsum(test_data)
  53.         return test_data, new_hashsum, method
  54.     else:
  55.         return data, hashsum, None
  56.  
  57.  
  58. def compress_png(image):
  59.     # 0) Пробуем просто пересохранить
  60.     min_stream = BytesIO()
  61.     image.save(min_stream, 'PNG', optimize=True)
  62.     min_size = len(min_stream.getvalue())
  63.     method = 'resave'
  64.  
  65.     # 1) Пробуем пересохранить с zlib (иногда почему-то меньше, чем optimize=True)
  66.     test_stream = BytesIO()
  67.     image.save(test_stream, 'PNG', compress_level=9)
  68.     test_size = len(test_stream.getvalue())
  69.     if test_size < min_size:
  70.         min_stream = test_stream
  71.         min_size = test_size
  72.         method = 'zlib'
  73.  
  74.     # 2) Пробуем закрасить чёрным невидимое
  75.     if image.mode == 'RGBA':
  76.         from PIL import ImageDraw
  77.         with image.copy() as test_image:
  78.             w = test_image.size[0]
  79.             draw = None
  80.             for i, pixel in enumerate(test_image.getdata()):
  81.                 if pixel[3] < 1:
  82.                     if draw is None:
  83.                         draw = ImageDraw.Draw(test_image)
  84.                     draw.point([(i % w, i // w)], (0, 0, 0, 0))
  85.             if draw is not None:
  86.                 test_stream = BytesIO()
  87.                 test_image.save(test_stream, 'PNG', optimize=True)
  88.                 test_size = len(test_stream.getvalue())
  89.                 if test_size < min_size:
  90.                     min_stream = test_stream
  91.                     min_size = test_size
  92.                     method = 'zeroalpha'
  93.             del draw
  94.  
  95.     return min_stream.getvalue(), method
  96.  
  97.  
  98. def find_pngs(path, recursive=False):
  99.     files = []
  100.     for x in sorted(os.listdir(path)):
  101.         xpath = os.path.join(path, x)
  102.         if os.path.isdir(xpath):
  103.             if recursive:
  104.                 files.extend(os.path.join(x, y) for y in find_pngs(xpath, True))
  105.             continue
  106.         if x.endswith('.png'):
  107.             files.append(x)
  108.     return files
  109.  
  110.  
  111. def compress_file(path, write_path=None):
  112.     print(path, end=' ')
  113.     sys.stdout.flush()
  114.  
  115.     try:
  116.         data = open(path, 'rb').read()
  117.     except:
  118.         print('Cannot read')
  119.         return False, None
  120.  
  121.     from PIL import Image
  122.     try:
  123.         image = Image.open(BytesIO(data))
  124.     except:
  125.         print('Cannot decode')
  126.         return False, None
  127.  
  128.     original_size = len(data)
  129.     with image:
  130.         compressed_data, hashsum, method = compress_image(data, calc_hashsum(data), image)
  131.     del image
  132.  
  133.     if not method:
  134.         print('Not compressed')
  135.         return True, None
  136.  
  137.     compressed_size = len(compressed_data)
  138.     percent = (1 - compressed_size / original_size) * 100
  139.  
  140.     if write_path:
  141.         with open(write_path, 'wb') as fp:
  142.             fp.write(compressed_data)
  143.  
  144.     print('OK: {method} {percent:.2f}% ({original_size} before - {diff_size} = {compressed_size} after)'.format(
  145.         method=method,
  146.         percent=percent,
  147.         original_size=original_size,
  148.         diff_size=original_size - compressed_size,
  149.         compressed_size=compressed_size
  150.     ))
  151.  
  152.     return True, (original_size, compressed_size)
  153.  
  154.  
  155. def main():
  156.     args = sys.argv[1:]
  157.  
  158.     if '-r' in args:
  159.         recursive = True
  160.         args.remove('-r')
  161.     else:
  162.         recursive = False
  163.  
  164.     if len(args) != 2:
  165.         print('Usage: {} [-r] input_dir output_dir'.format(sys.argv[0]), file=sys.stderr)
  166.         print('Reads PNG files from input directory,')
  167.         print('compress it and writes to output directory')
  168.         print('(can be same). Recursive with "-r".')
  169.         sys.exit(2)
  170.  
  171.     input_dir, output_dir = args
  172.  
  173.     files = find_pngs(input_dir, recursive)
  174.     success = 0
  175.     count = len(files)
  176.  
  177.     results = []
  178.  
  179.     for x in files:
  180.         try:
  181.             processed, result = compress_file(os.path.join(input_dir, x), os.path.join(output_dir, x))
  182.         except:
  183.             print()
  184.             print(traceback.format_exc())
  185.             continue
  186.  
  187.         if processed:
  188.             success += 1
  189.             if result is not None:
  190.                 results.append(result)
  191.  
  192.         sys.stdout.flush()
  193.  
  194.     if success < count:
  195.         print('{} files failed, please check it'.format(count - success))
  196.         sys.exit(1)
  197.  
  198.     percents = [(100.0 - (x[1] / x[0]) * 100.0) for x in results]
  199.  
  200.     if len(percents) >= len(files):
  201.         print('Average compression {:.2f}%'.format(sum(percents) / len(percents)))
  202.     elif percents:
  203.         tprc = percents + ([0.0] * (len(files) - len(percents)))
  204.         print('Average compression {:.2f}% ({:.2f}% considering uncompressed)'.format(
  205.             sum(percents) / len(percents),
  206.             sum(tprc) / len(tprc)
  207.         ))
  208.     if percents:
  209.         print('{:.2f} KiB -> {:.2f} KiB'.format(sum(x[0] for x in results) / 1024, sum(x[1] for x in results) / 1024))
  210.  
  211.  
  212. if __name__ == '__main__':
  213.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement