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()