cmd/resize.py

236 lines
7.6 KiB
Python
Raw Normal View History

2021-08-01 23:21:27 +00:00
'''
resize
======
Resize image files.
2022-01-10 05:50:47 +00:00
> resize patterns <flags>
2022-01-10 05:50:47 +00:00
patterns:
One or more glob patterns for input files.
Uses pipeable to support !c clipboard, !i stdin lines of glob patterns.
2021-08-01 23:21:27 +00:00
flags:
2022-01-10 05:50:47 +00:00
--width X:
--height X:
New dimensions for the image. If either of these is omitted, then that
dimension will be calculated automatically based on the aspect ratio.
--break_aspect_ratio:
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.
--output X:
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
2022-01-10 05:50:47 +00:00
{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.
2021-11-15 06:00:45 +00:00
2021-08-01 23:21:27 +00:00
--inplace:
2022-01-10 05:50:47 +00:00
Overwrite the input files. Cannot be used along with --output.
Be careful!
2021-08-01 23:21:27 +00:00
--nearest:
2022-01-10 05:50:47 +00:00
If provided, use nearest-neighbor scaling to preserve pixelated images.
If omitted, use antialiased scaling.
2021-08-01 23:21:27 +00:00
--only_shrink:
If the input image is smaller than the requested dimensions, do nothing.
Useful when globbing in a directory with many differently sized images.
--quality X:
JPEG compression quality.
--scale X:
2022-01-10 05:50:47 +00:00
Scale the image by factor X.
Use this option instead of --width, --height.
2021-08-01 23:21:27 +00:00
'''
2020-07-01 03:27:35 +00:00
import argparse
2022-01-10 05:50:47 +00:00
import os
import PIL.Image
2019-06-12 05:41:31 +00:00
import sys
2021-08-01 23:21:27 +00:00
from voussoirkit import betterhelp
from voussoirkit import imagetools
2021-03-03 20:39:39 +00:00
from voussoirkit import pathclass
from voussoirkit import pipeable
2022-01-10 05:50:47 +00:00
from voussoirkit import sentinel
from voussoirkit import vlogging
log = vlogging.getLogger(__name__, 'resize')
2022-01-10 05:50:47 +00:00
OUTPUT_INPLACE = sentinel.Sentinel('output inplace')
DEFAULT_OUTPUT_FORMAT = '{base}_{width}x{height}{extension}'
def resize_core(
image,
height=None,
only_shrink=False,
scale=None,
width=None,
):
pass
def resize(
filename,
*,
2022-01-10 05:50:47 +00:00
output_format=DEFAULT_OUTPUT_FORMAT,
height=None,
keep_aspect_ratio=True,
nearest_neighbor=False,
2020-10-23 14:48:31 +00:00
only_shrink=False,
2021-03-14 04:32:43 +00:00
quality=100,
scale=None,
2022-01-10 05:50:47 +00:00
width=None,
):
2022-01-10 05:50:47 +00:00
if scale and (width or height):
raise ValueError('Cannot use both scale and width/height.')
2021-03-03 20:39:39 +00:00
file = pathclass.Path(filename)
image = PIL.Image.open(file.absolute_path)
2019-06-12 05:41:31 +00:00
(image_width, image_height) = image.size
2019-06-12 05:41:31 +00:00
2022-01-10 05:50:47 +00:00
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.
2020-07-01 03:27:35 +00:00
pass
2022-01-10 05:50:47 +00:00
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,
2020-10-23 14:48:31 +00:00
only_shrink=only_shrink,
)
2022-01-10 05:50:47 +00:00
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,
2020-10-23 14:48:31 +00:00
only_shrink=only_shrink,
)
2020-07-01 03:27:35 +00:00
else:
2022-01-10 05:50:47 +00:00
raise ValueError('Insufficient parameters for resizing. Need width, height, or scale.')
2022-01-10 05:50:47 +00:00
if output_format is OUTPUT_INPLACE:
output_file = file
2021-11-15 06:00:45 +00:00
else:
2022-01-10 05:50:47 +00:00
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()
2021-11-15 06:00:45 +00:00
filename = file.basename
2022-01-10 05:50:47 +00:00
output_file = output_format.format(
base=file.replace_extension('').basename,
2022-01-10 05:50:47 +00:00
extension=file.extension.with_dot,
filename=file.basename,
height=height,
width=width,
2022-01-10 05:50:47 +00:00
)
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)
2022-01-10 05:50:47 +00:00
if output_file == file:
raise ValueError('Cannot overwrite input file without OUTPUT_INPLACE.')
2021-03-03 20:39:39 +00:00
2022-01-10 05:50:47 +00:00
log.debug('Resizing %s to %dx%d.', file.absolute_path, width, height)
resampler = PIL.Image.NEAREST if nearest_neighbor else PIL.Image.ANTIALIAS
image = image.resize( (width, height), resampler)
if output_file.extension == '.jpg':
image = image.convert('RGB')
2020-07-01 03:27:35 +00:00
2022-01-10 05:50:47 +00:00
image.save(output_file.absolute_path, exif=image.getexif(), quality=quality)
return output_file
2020-07-01 03:27:35 +00:00
def resize_argparse(args):
2022-01-10 05:50:47 +00:00
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:
2022-01-10 05:50:47 +00:00
output_file = resize(
file,
2022-01-10 05:50:47 +00:00
height=args.height,
keep_aspect_ratio=not args.break_aspect_ratio,
2020-10-23 14:48:13 +00:00
nearest_neighbor=args.nearest_neighbor,
2020-10-23 14:48:31 +00:00
only_shrink=args.only_shrink,
2022-01-10 05:50:47 +00:00
output_format=output_format,
2021-03-14 04:32:43 +00:00
quality=args.quality,
scale=args.scale,
2022-01-10 05:50:47 +00:00
width=args.width,
2020-07-01 03:27:35 +00:00
)
2022-01-10 05:50:47 +00:00
pipeable.stdout(output_file.absolute_path)
2020-07-01 03:27:35 +00:00
return 0
@vlogging.main_decorator
2020-07-01 03:27:35 +00:00
def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
2022-01-10 05:50:47 +00:00
parser.add_argument('patterns', nargs='+')
parser.add_argument('--width', type=int, default=None)
parser.add_argument('--height', type=int, default=None)
parser.add_argument('--inplace', action='store_true')
2020-10-23 14:48:13 +00:00
parser.add_argument('--nearest', dest='nearest_neighbor', action='store_true')
parser.add_argument('--only_shrink', '--only-shrink', action='store_true')
2022-01-10 05:50:47 +00:00
parser.add_argument('--break_aspect_ratio', '--break-aspect-ratio', action='store_true')
parser.add_argument('--output', default=None)
parser.add_argument('--scale', type=float, default=None)
2021-03-14 04:32:43 +00:00
parser.add_argument('--quality', type=int, default=100)
2020-07-01 03:27:35 +00:00
parser.set_defaults(func=resize_argparse)
2021-08-01 23:21:27 +00:00
return betterhelp.single_main(argv, parser, __doc__)
2020-07-01 03:27:35 +00:00
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))