Skip to content

Instantly share code, notes, and snippets.

@kkew3
Created December 29, 2022 10:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kkew3/ed7eebdc5827d34de9b7ee20e3d1defe to your computer and use it in GitHub Desktop.
Save kkew3/ed7eebdc5827d34de9b7ee20e3d1defe to your computer and use it in GitHub Desktop.
Compress image to just below the given size upper bound with `imagemagick` using bisection algorithm.
#!/usr/bin/env python3
import os
import re
import sys
import shlex
import shutil
import argparse
import subprocess
import logging
import more_bisect
class Key:
__slots__ = 'infile', 'outfile'
def __init__(self, infile, outfile):
self.infile = infile
self.outfile = outfile
def __call__(self, pcnt: int):
"""
:param pcnt: in range [0, 100)
"""
pcnt += 1
if pcnt == 100:
print('magick {} {}'.format(
shlex.quote(self.infile), shlex.quote(self.outfile)))
args = ['magick', self.infile, self.outfile]
subprocess.run(args, check=True)
return os.path.getsize(self.outfile)
print('magick {} -resize {}% {}'.format(
shlex.quote(self.infile), pcnt, shlex.quote(self.outfile)))
args = [
'magick',
self.infile,
'-resize',
str(pcnt) + '%',
self.outfile,
]
subprocess.run(args, check=True)
return os.path.getsize(self.outfile)
def make_parser():
parser = argparse.ArgumentParser(
description=('Resize image to just fit the given upper bound. When '
'`size_upper_bound` is near the original size, the '
'`outfile` size may not be exactly below the upper bound '
'due to issue with `magick`.'))
parser.add_argument(
'size_upper_bound',
help=(
'e.g., 2 as 2 bytes, 2B as 2 bytes, 2K as 2000 bytes, 2KB as 2000 '
'bytes, 2Ki as 2048 bytes, 2KiB as 2048 bytes, 2M as 2000000 '
'bytes, 2MB as 2000000 bytes, 2Mi as 2097152 bytes, 2MiB as '
'2097152 bytes, etc. Supported suffixes: B, K, M, G, T, Ki, Mi, '
'Gi, Ti, KiB, MiB, GiB, TiB'))
parser.add_argument('infile')
parser.add_argument('outfile')
return parser
def parse_size(ssize: str) -> int:
matched = re.match(r'([\d\.]+)', ssize)
if not matched:
raise ValueError
n = matched.group(1)
sfx = ssize[len(n):]
n = float(n)
if sfx.endswith('B'):
sfx = sfx[:-1]
try:
m = {
'': 1,
'K': 1000,
'Ki': 1024,
'M': 1000**2,
'Mi': 1024**2,
'G': 1000**3,
'Gi': 1024**3,
'T': 1000**4,
'Ti': 1024**4,
}[sfx]
except KeyError as err:
raise ValueError from err
n = int(n * m)
return n
def main():
logging.basicConfig(format='%(levelname)s: %(message)s')
args = make_parser().parse_args()
try:
ub = parse_size(args.size_upper_bound)
except ValueError:
logging.error('failed to parse `%s`', args.size_upper_bound)
return 1
try:
more_bisect.last_pos_lt(
ub, lo=0, hi=100, key=Key(args.infile, args.outfile))
except subprocess.CalledProcessError:
logging.error('failed to run magick commands')
return 2
return 0
if __name__ == '__main__':
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment