[kde-artists] PS: proposal to losslessly shrink jpeg files

Martin Walch <[email protected]>
Newsgroups gmane.comp.kde.artists
Message-ID <1719186.K4Z2jDXumA@tacticalops>
Sorry. Forgot to re-attach the file.

______________________________________________________________________________
[email protected] |  https://mail.kde.org/mailman/listinfo/kde-artists
compress-jpg.py (text/x-python, 5 KB)
#!/usr/bin/python
# -*- coding: utf_8 -*-

# version 0.1
# Copyright 2012 Martin Walch
# redistributable under the terms of the GNU GPL version 3

import argparse
import os
import signal
import stat
import subprocess
import sys
import tempfile

def clean_file(filename):
    """make sure the file filename is deleted"""
    if os.path.exists(filename):
        os.unlink(filename)

def new_file(filename):
    """make sure any old file is deleted and create a new file"""
    clean_file(filename)
    return open(filename, mode='wb')

def merge_new_file(path, new_file):
    """Replace the original file if the new file is smaller."""
    new_file.close()
    size_old = os.stat(path).st_size
    size_new = os.stat(new_file.name).st_size
    if size_new < size_old:
        if arguments.keep_timestamp:
            # touch from coreutils (http://www.gnu.org/software/coreutils/)
            subprocess.call(["touch", "-r", path, new_file.name])
        os.chmod(new_file.name, os.stat(path).st_mode)
        os.rename(new_file.name, path)
        if not arguments.quiet:
            print(' ' + str(size_old) + ' -> ' + str(size_new))
    else:
        clean_file(new_file.name)

def jpegtran(path, output_file, progressive):
    """try optimization with jpegtran"""
    # jpegtran from IJG's jpeg implementation (http://www.ijg.org/)
    # or from libjpeg-turbo (http://libjpeg-turbo.virtualgl.org/)
    argv = ["jpegtran", "-o", "-copy", "all", path]
    if progressive:
        argv.insert(1, "-progressive")
    status = subprocess.call(argv, stdout=output_file)
    if status == 0:
        merge_new_file(path, output_file)
    elif not arguments.quiet:
        print('Warning: jpegtran failed. Maybe the input file is corrupt?')
        

def exif(path, output_file):
    """try removal of embedded thumbnails"""
    sink = open('/dev/null', 'w')
    # exif from libexif (http://libexif.sourceforge.net/)
    status = subprocess.call(
        ["exif", "--no-fixup", "-r", "-o", output_file.name, path],
        stdout=sink,
        stderr=subprocess.STDOUT)
    if status == 0:
        merge_new_file(path, output_file)

def is_jpeg(path):
    """return True iff file that is located at path is a jpeg file"""
    # the widely used file command (https://github.com/glensc/file)
    filetype = subprocess.check_output(["file", "-bi", path])
    if filetype[:11].decode(sys.stdout.encoding) == "image/jpeg;":
        return True
    else:
        return False

def process_file(path):
    """process a single jpeg file"""
    global name_temp_file
    temp_file = tempfile.NamedTemporaryFile(
        dir=os.path.dirname(path),
        delete=True)
    name_temp_file = temp_file.name
    temp_file.close()
    
    if not arguments.quiet:
        print(path)
    
    temp_file = new_file(name_temp_file)
    jpegtran(path, temp_file, False)
    
    if arguments.progressive:
        temp_file = new_file(name_temp_file)
        jpegtran(path, temp_file, True)
    if arguments.drop_thumbnail:
        temp_file = new_file(name_temp_file)
        exif(path, temp_file)
    
    clean_file(name_temp_file)

def process_dir(path):
    """traverse the file system hierarchy below the directory path"""
    spinner = '|/-\\-/'
    spinner_pos = 0
    for directory, subdirectories, files in os.walk(path):
        for next_file in files:
            file_path = os.path.join(directory, next_file)
            if not arguments.quiet:
                sys.stdout.write('\r' + spinner[spinner_pos])
                spinner_pos = (spinner_pos + 1) % len(spinner)
                sys.stdout.flush()
            if is_jpeg(file_path):
                sys.stdout.write('\r')
                process_file(file_path)

def signal_handler(signum, frame):
    """catch termination request to clean up before exiting, skip backtrace"""
    print('\ntermination signal caught, cleaning up and exiting…')
    if 'name_temp_file' in globals() and os.path.exists(name_temp_file):
        os.unlink(name_temp_file)
    sys.exit(-1)

def main():
    global arguments
    
    parser = argparse.ArgumentParser()
    parser.add_argument(
        'target',
        type=str)
    parser.add_argument(
        '--no-progressive',
        dest='progressive',
        action="store_false")
    parser.add_argument(
        '--keep-thumbnail',
        dest='drop_thumbnail',
        action="store_false")
    parser.add_argument(
        '--new-timestamp',
        dest='keep_timestamp',
        action="store_true")
    parser.add_argument(
        '--quiet',
        dest='quiet',
        action="store_true")
    
    arguments = parser.parse_args()
    target = os.path.abspath(arguments.target)
    
    mode = os.stat(target).st_mode
    
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    
    if stat.S_ISREG(mode):
        if is_jpeg(target):
            process_file(target)
        else:
            print('not a jpeg (the file command did not identify it as such)')
    elif stat.S_ISDIR(mode):
        process_dir(target)

main()
signature.asc (application/pgp-signature, 198 B)
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.17 (GNU/Linux)

iEYEABECAAYFAk/ACNsACgkQM6S4K8IXn9deFwCeIzWRRwYv6fbBoCMRBIDXLoWV
MPQAn1YqaDazP7pbC2xRyj03zXjh/Sio
=a8fC
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.