Scale an image to fit under a certain size
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()