cmd/resize.py

271 lines
8.6 KiB
Python

import argparse
import os
import PIL.Image
import sys
from voussoirkit import betterhelp
from voussoirkit import imagetools
from voussoirkit import pathclass
from voussoirkit import pipeable
from voussoirkit import sentinel
from voussoirkit import vlogging
log = vlogging.getLogger(__name__, 'resize')
OUTPUT_INPLACE = sentinel.Sentinel('output inplace')
DEFAULT_OUTPUT_FORMAT = '{base}_{width}x{height}{extension}'
def resize(
filename,
*,
output_format=DEFAULT_OUTPUT_FORMAT,
height=None,
keep_aspect_ratio=True,
nearest_neighbor=False,
only_shrink=False,
quality=100,
scale=None,
width=None,
):
if scale and (width or height):
raise ValueError('Cannot use both scale and width/height.')
file = pathclass.Path(filename)
image = PIL.Image.open(file.absolute_path)
icc_profile = image.info.get('icc_profile')
(image, exif) = imagetools.rotate_by_exif(image)
(image_width, image_height) = image.size
if scale:
width = int(image_width * scale)
height = int(image_height * scale)
elif (width and height) and not keep_aspect_ratio:
# The given width and height will be used exactly.
pass
elif (width and height) and keep_aspect_ratio:
(width, height) = imagetools.fit_into_bounds(
image_width=image_width,
image_height=image_height,
frame_width=width,
frame_height=height,
only_shrink=only_shrink,
)
elif (width and not height) and keep_aspect_ratio:
(width, height) = imagetools.fit_into_bounds(
image_width=image_width,
image_height=image_height,
frame_width=width,
frame_height=10000000,
only_shrink=only_shrink,
)
elif (height and not width) and keep_aspect_ratio:
(width, height) = imagetools.fit_into_bounds(
image_width=image_width,
image_height=image_height,
frame_width=10000000,
frame_height=height,
only_shrink=only_shrink,
)
else:
raise ValueError('Insufficient parameters for resizing. Need width, height, or scale.')
if output_format is OUTPUT_INPLACE:
output_file = file
else:
output_format = pathclass.normalize_sep(output_format)
if output_format.endswith(os.sep):
output_folder = pathclass.Path(output_format)
output_format = DEFAULT_OUTPUT_FORMAT
elif os.sep in output_format:
full = pathclass.Path(output_format)
output_folder = full.parent
output_format = full.basename
else:
output_folder = file.parent
output_folder.assert_is_directory()
filename = file.basename
output_file = output_format.format(
base=file.replace_extension('').basename,
extension=file.extension.with_dot,
filename=file.basename,
height=height,
width=width,
)
output_file = output_folder.with_child(output_file)
known_extensions = {os.path.normcase(ext) for ext in PIL.Image.registered_extensions()}
output_norm = output_file.normcase
if not any(output_norm.endswith(ext) for ext in known_extensions):
output_file = output_file.add_extension(file.extension)
if output_file == file:
raise ValueError('Cannot overwrite input file without OUTPUT_INPLACE.')
log.debug('Resizing %s to %dx%d.', file.absolute_path, width, height)
resampler = PIL.Image.NEAREST if nearest_neighbor else PIL.Image.LANCZOS
if image.mode == '1':
image = image.convert('L')
image = image.resize( (width, height), resampler)
if output_file.extension == '.jpg':
image = image.convert('RGB')
image.save(output_file.absolute_path, exif=exif, quality=quality, icc_profile=icc_profile)
return output_file
def resize_argparse(args):
if args.inplace and args.output:
pipeable.stderr('Cannot have both --inplace and --output')
return 1
if args.inplace:
output_format = OUTPUT_INPLACE
elif args.output:
output_format = args.output
else:
output_format = DEFAULT_OUTPUT_FORMAT
patterns = pipeable.input_many(args.patterns, skip_blank=True, strip=True)
files = pathclass.glob_many_files(patterns)
for file in files:
output_file = resize(
file,
height=args.height,
keep_aspect_ratio=not args.break_aspect_ratio,
nearest_neighbor=args.nearest_neighbor,
only_shrink=args.only_shrink,
output_format=output_format,
quality=args.quality,
scale=args.scale,
width=args.width,
)
pipeable.stdout(output_file.absolute_path)
return 0
@vlogging.main_decorator
def main(argv):
parser = argparse.ArgumentParser(description='Resize image files.')
parser.examples = [
'myphoto.jpg --scale 0.5 --inplace',
'*.jpg --only_shrink --width 500 --height 500 --output thumbs\\{base}.jpg',
'sprite*.png sprite*.bmp --width 1024 --nearest --output {base}_big{extension}',
]
parser.add_argument(
'patterns',
nargs='+',
type=str,
help='''
One or more glob patterns for input files.
Uses pipeable to support !c clipboard, !i stdin lines of glob patterns.
''',
)
parser.add_argument(
'--width',
type=int,
default=None,
help='''
New width of the image. If --width is omitted and --height is given, then
width will be calculated automatically based on the aspect ratio.
''',
)
parser.add_argument(
'--height',
type=int,
default=None,
help='''
New width of the image. If --height is omitted and --width is given, then
height will be calculated automatically based on the aspect ratio.
''',
)
parser.add_argument(
'--break_aspect_ratio',
'--break-aspect-ratio',
action='store_true',
help='''
If provided, the given --width and --height will be used exactly. You will
need to provide both --width and --height.
If omitted, the image will be resized to fit within the bounds provided by
--width and --height while preserving its aspect ratio.
''',
)
parser.add_argument(
'--inplace',
action='store_true',
help='''
Overwrite the input files. Cannot be used along with --output.
Be careful!
''',
)
parser.add_argument(
'--nearest',
'--nearest_neighbor',
'--nearest-neighbor',
dest='nearest_neighbor',
action='store_true',
help='''
If provided, use nearest-neighbor scaling to preserve pixelated images.
If omitted, use antialiased scaling.
''',
)
parser.add_argument(
'--only_shrink',
'--only-shrink',
action='store_true',
help='''
If the input image is smaller than the requested dimensions, do nothing.
Useful when globbing in a directory with many differently sized images.
''',
)
parser.add_argument(
'--output',
default=None,
help='''
A string that controls the output filename format. Suppose the input file
was myphoto.jpg. You can use these variables in your format string:
{base} = myphoto
{filename} = myphoto.jpg
{width} = an integer
{height} = an integer
{extension} = .jpg
You may omit {extension} from your format string and it will automatically
be added to the end, unless you already provided a different extension.
If your format string only designates a basename, output files will go to
the same directory as the corresponding input file. If your string contains
path separators, all output files will go to that directory.
The directory part is not formatted with the variables.
''',
)
parser.add_argument(
'--scale',
type=float,
default=None,
help='''
Scale the image by this factor, where 1.00 is regular size.
Use this option instead of --width, --height.
''',
)
parser.add_argument(
'--quality',
type=int,
default=100,
help='''
JPEG compression quality.
'''
)
parser.set_defaults(func=resize_argparse)
return betterhelp.go(parser, argv)
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))