Technomancy

Entries tagged “python.cli”

Scale an image to fit under a certain size

written by rory, on Jan 7, 2009 5:07:00 PM.

Many times you have an image that is of a certain size, but you need it to be less than a certain size. For example, photos uploaded to Flickr must be under 20 MiB. You can take a guess as to how much you can resize an image by, however most of the time you would like the image as large as possible, but still be under the required size. This is a little script that will try to find the largest resize that keeps the image under a certain size. It does this by doing a binary search on possible resize values (ie from 0% to 100%). It will always return an image that is less than or equal to the required max size.

Usage

$ image-max-size.py -h
Usage: image-max-size.py [options]

Options:
  -h, --help            show this help message and exit
  -s SIZE, --size=SIZE  Max size of the image (in MiB)
  -i IMAGE, --input-image=IMAGE
                        Filename of the image to use
  -o FILENAME, --output-image=FILENAME
                        Filename of the resultant image, it must not already
                        exist
  -a ACCURACY, --accuracy=ACCURACY
                        The accuracy to try for. This is a floating point
                        number between 0.0 and 1.0. The lower the value the
                        longer the programme will run for, but the result will
                        be closer to the target size. The default of 0.01 is
                        usually adequate.

Example Usage

$ image-max-size.py -i big-image.png -o smaller-image.png -s 10
Input file is of size 21.55 MiB which is 11.55 MiB (2.2x) too large
Scaling by 50% gives a size of 5.43 MiB, the file could be 4.57 MiB bigger
Scaling by 75% gives a size of 12.06 MiB, which is 2.06 MiB too large
Scaling by 62% gives a size of 8.30 MiB, the file could be 1.70 MiB bigger
Scaling by 68% gives a size of 9.96 MiB, the file could be 0.04 MiB bigger
Scaling by 71% gives a size of 10.84 MiB, which is 0.84 MiB too large
Scaling by 70% gives a size of 10.54 MiB, which is 0.54 MiB too large
Scaling by 69% gives a size of 10.25 MiB, which is 0.25 MiB too large
Scaling by 68% gives a size of 9.957MiB

The code

#! /usr/bin/python

import optparse, sys, os, os.path, tempfile, shutil, commands

def main():
    parser = optparse.OptionParser()
    parser.add_option("-s", "--size", help="Max size of the image (in MiB)", type='float')
    parser.add_option("-i", "--input-image", help="Filename of the image to use", metavar="IMAGE")
    parser.add_option("-o", "--output-image", help="Filename of the resultant image, it must not already exist", metavar="FILENAME")
    parser.add_option("-a", "--accuracy", help="The accuracy to try for. This is a floating point number between 0.0 and 1.0. The lower the value the longer the programme will run for, but the result will be closer to the target size. The default of 0.01 is usually adequate.", default=0.01, type="float")

    (options, args) = parser.parse_args()

    required = ['size', 'input_image', 'output_image']
    if not all(getattr(options, x) is not None for x in required):
        parser.print_help()
        sys.exit(1)

    if not os.path.isfile(options.input_image):
        print "Input file %s does not exist" % options.input_image
        parser.print_help()
        sys.exit(2)

    if os.path.isfile(options.output_image):
        print "Output file %s exists." % options.output_image
        parser.print_help()
        sys.exit(2)

    input = os.path.abspath(options.input_image)

    size = options.size

    # get the filesize in mebibytes
    file_size = float(os.path.getsize(input)) / 1024 / 1024
    print "Input file is of size %.2f MiB which is %.2f MiB (%.1fx) too large" % (file_size, file_size - size, file_size/size)

    upper = 1.0
    lower = 0.0

    file_extension = os.path.splitext(input)[1]

    # This is the temporary file for the scales
    temp_file_socket, temp_file_name = tempfile.mkstemp(prefix="scale_image_", suffix=file_extension)

    # A temporary file for the last file that's under the size limit.
    last_good_file_socket, last_good_file_name = tempfile.mkstemp(prefix="scale_image_last_good_", suffix=file_extension)
    last_good_attempt = None
    last_good_size = None

    shutil.copyfile(input, temp_file_name)

    while upper - lower > options.accuracy:
        attempt = ((upper - lower) / 2 ) + lower
        assert lower <= attempt <= upper
        status, output = commands.getstatusoutput(
            "convert -geometry %d%% \"%s\" \"%s\"" % (attempt * 100, input, temp_file_name))
        assert status == 0, output
        file_size = float(os.path.getsize(temp_file_name)) / 1024 / 1024
        if file_size > size:
            print (
                "Scaling by %2d%% gives a size of %.2f MiB, which is %.2f MiB too large"
                % (attempt * 100, file_size, file_size-size) )
            upper = attempt

        elif file_size < size:
            print (
                "Scaling by %2d%% gives a size of %.2f MiB, the file could be %.2f MiB bigger"
                % (attempt * 100, file_size, size-file_size) )
            lower = attempt
            shutil.copyfile(temp_file_name, last_good_file_name)
            last_good_attempt = attempt
            last_good_size = file_size

        elif file_size == size:
            print "Scaling by %d%% gives a size of %.2f MiB, which is just right" % (attempt * 100, file_size)
            shutil.copyfile(temp_file_name, last_good_file_name)
            break

    print "Scaling by %d%% gives a size of %.3fMiB" % (last_good_attempt*100, last_good_size)

    os.rename(last_good_file_name, options.output_image)

    # cleanup
    os.remove(temp_file_name)

if __name__ == '__main__':
    main()