Big rewrite of resize.py.

This commit is contained in:
voussoir 2022-01-09 21:50:47 -08:00
parent 57f80ecd90
commit d82733c8d5
No known key found for this signature in database
GPG key ID: 5F7554F8C26DACCB

208
resize.py
View file

@ -4,27 +4,49 @@ resize
Resize image files. Resize image files.
> resize pattern new_w new_h <flags> > resize patterns <flags>
pattern: patterns:
Glob pattern for input files. One or more glob patterns for input files.
Uses pipeable to support !c clipboard, !i stdin lines of glob patterns.
new_w,
new_h:
New dimensions for the image. If either of these is 0, then that
dimension will be calculated by resizing the other side while keeping the
aspect ratio.
flags: flags:
--destination: --width X:
A path to a directory where the png files should be saved. By default, --height X:
they go to the same folder as the input file. 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
{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.
--inplace: --inplace:
Overwrite the input files, instead of creating _WxH names. Overwrite the input files. Cannot be used along with --output.
Be careful!
--nearest: --nearest:
Use nearest-neighbor scaling to preserve pixelated images. If provided, use nearest-neighbor scaling to preserve pixelated images.
If omitted, use antialiased scaling.
--only_shrink: --only_shrink:
If the input image is smaller than the requested dimensions, do nothing. If the input image is smaller than the requested dimensions, do nothing.
@ -34,9 +56,11 @@ flags:
JPEG compression quality. JPEG compression quality.
--scale X: --scale X:
Use this option instead of new_w, new_h. Scale the image by factor X. Scale the image by factor X.
Use this option instead of --width, --height.
''' '''
import argparse import argparse
import os
import PIL.Image import PIL.Image
import sys import sys
@ -44,91 +68,147 @@ from voussoirkit import betterhelp
from voussoirkit import imagetools from voussoirkit import imagetools
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import pipeable from voussoirkit import pipeable
from voussoirkit import sentinel
from voussoirkit import vlogging from voussoirkit import vlogging
log = vlogging.getLogger(__name__, 'resize') log = vlogging.getLogger(__name__, 'resize')
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( def resize(
filename, filename,
new_w=None,
new_h=None,
*, *,
destination=None, destination=None,
inplace=False, output_format=DEFAULT_OUTPUT_FORMAT,
height=None,
keep_aspect_ratio=True,
nearest_neighbor=False, nearest_neighbor=False,
only_shrink=False, only_shrink=False,
quality=100, quality=100,
scale=None, scale=None,
width=None,
): ):
if scale and (width or height):
raise ValueError('Cannot use both scale and width/height.')
file = pathclass.Path(filename) file = pathclass.Path(filename)
image = PIL.Image.open(file.absolute_path) image = PIL.Image.open(file.absolute_path)
(image_width, image_height) = image.size (image_width, image_height) = image.size
if new_w is not None and new_h is not None: 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 pass
elif scale: elif (width and height) and keep_aspect_ratio:
new_w = int(image_width * scale) (width, height) = imagetools.fit_into_bounds(
new_h = int(image_height * scale) image_width=image_width,
image_height=image_height,
if new_w == 0: frame_width=width,
(new_w, new_h) = imagetools.fit_into_bounds( frame_height=height,
image_width,
image_height,
10000000,
new_h,
only_shrink=only_shrink, only_shrink=only_shrink,
) )
if new_h == 0: elif (width and not height) and keep_aspect_ratio:
(new_w, new_h) = imagetools.fit_into_bounds( (width, height) = imagetools.fit_into_bounds(
image_width, image_width=image_width,
image_height, image_height=image_height,
new_w, frame_width=width,
10000000, 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, only_shrink=only_shrink,
) )
log.debug('Resizing %s to %dx%d.', file.absolute_path, new_w, new_h)
if nearest_neighbor:
image = image.resize( (new_w, new_h), PIL.Image.NEAREST)
else: else:
image = image.resize( (new_w, new_h), PIL.Image.ANTIALIAS) raise ValueError('Insufficient parameters for resizing. Need width, height, or scale.')
if destination is None: if output_format is OUTPUT_INPLACE:
destination = file.parent output_file = file
else: else:
destination = pathclass.Path(destination) output_format = pathclass.normalize_sep(output_format)
destination.assert_is_directory() 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()
if inplace:
new_name = destination.with_child(file.basename)
else:
suffix = '_{width}x{height}'.format(width=new_w, height=new_h)
base = file.replace_extension('').basename base = file.replace_extension('').basename
new_name = base + suffix + file.extension.with_dot if '{extension}' not in output_format:
new_name = destination.with_child(new_name) known_extensions = PIL.Image.registered_extensions()
known_extensions = {os.path.normcase(ext) for ext in known_extensions}
output_norm = os.path.normcase(output_format)
if not any(output_norm.endswith(ext) for ext in known_extensions):
output_format += '{extension}'
output_file = output_format.format(
base=base,
width=width,
height=height,
extension=file.extension.with_dot,
)
output_file = output_folder.with_child(output_file)
if output_file == file:
raise ValueError('Cannot overwrite input file without OUTPUT_INPLACE.')
if new_name.extension == '.jpg': 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') image = image.convert('RGB')
pipeable.stdout(new_name.absolute_path) image.save(output_file.absolute_path, exif=image.getexif(), quality=quality)
image.save(new_name.absolute_path, exif=image.getexif(), quality=quality) return output_file
def resize_argparse(args): def resize_argparse(args):
patterns = pipeable.input(args.pattern, skip_blank=True, strip=True) 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) files = pathclass.glob_many_files(patterns)
for file in files: for file in files:
resize( output_file = resize(
file, file,
args.new_w, height=args.height,
args.new_h, keep_aspect_ratio=not args.break_aspect_ratio,
destination=args.destination,
inplace=args.inplace,
nearest_neighbor=args.nearest_neighbor, nearest_neighbor=args.nearest_neighbor,
only_shrink=args.only_shrink, only_shrink=args.only_shrink,
output_format=output_format,
quality=args.quality, quality=args.quality,
scale=args.scale, scale=args.scale,
width=args.width,
) )
pipeable.stdout(output_file.absolute_path)
return 0 return 0
@ -136,13 +216,15 @@ def resize_argparse(args):
def main(argv): def main(argv):
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('pattern') parser.add_argument('patterns', nargs='+')
parser.add_argument('new_w', nargs='?', type=int, default=None) parser.add_argument('--width', type=int, default=None)
parser.add_argument('new_h', nargs='?', type=int, default=None) parser.add_argument('--height', type=int, default=None)
parser.add_argument('--destination', nargs='?', default=None) parser.add_argument('--destination', default=None)
parser.add_argument('--inplace', action='store_true') parser.add_argument('--inplace', action='store_true')
parser.add_argument('--nearest', dest='nearest_neighbor', action='store_true') parser.add_argument('--nearest', dest='nearest_neighbor', action='store_true')
parser.add_argument('--only_shrink', '--only-shrink', action='store_true') parser.add_argument('--only_shrink', '--only-shrink', action='store_true')
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) parser.add_argument('--scale', type=float, default=None)
parser.add_argument('--quality', type=int, default=100) parser.add_argument('--quality', type=int, default=100)
parser.set_defaults(func=resize_argparse) parser.set_defaults(func=resize_argparse)