cmd/stitch.py

139 lines
4.5 KiB
Python

import PIL.Image
import argparse
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__, 'stitch')
VERTICAL = sentinel.Sentinel('vertical')
HORIZONTAL = sentinel.Sentinel('horizontal')
def stitch_argparse(args):
patterns = pipeable.input_many(args.image_files, skip_blank=True, strip=True)
files = pathclass.glob_many_files(patterns)
images = [PIL.Image.open(file.absolute_path) for file in files]
images = [imagetools.rotate_by_exif(image)[0] for image in images]
if args.grid:
(grid_x, grid_y) = [int(part) for part in args.grid.split('x')]
if grid_x * grid_y < len(images):
pipeable.stderr(f'Your grid {grid_x}x{grid_y} is too small for {len(images)} images.')
return 1
elif args.vertical:
grid_x = 1
grid_y = len(images)
else:
grid_x = len(images)
grid_y = 1
# We produce a 2D list of images which will become their final arrangement,
# and calculate the size of each row and column to accommodate the largest
# member of each.
arranged_images = [[] for y in range(grid_y)]
column_widths = [1 for x in range(grid_x)]
row_heights = [1 for x in range(grid_y)]
index_x = 0
index_y = 0
for image in images:
arranged_images[index_y].append(image)
column_widths[index_x] = max(column_widths[index_x], image.size[0])
row_heights[index_y] = max(row_heights[index_y], image.size[1])
if args.vertical:
index_y += 1
(bump_x, index_y) = divmod(index_y, grid_y)
index_x += bump_x
else:
index_x += 1
(bump_y, index_x) = divmod(index_x, grid_x)
index_y += bump_y
final_width = sum(column_widths) + ((grid_x - 1) * args.gap)
final_height = sum(row_heights) + ((grid_y - 1) * args.gap)
background = '#' + args.background.strip('#')
final_image = PIL.Image.new('RGBA', [final_width, final_height], color=background)
offset_y = 0
for (index_y, row) in enumerate(arranged_images):
offset_x = 0
for (index_x, image) in enumerate(row):
pad_x = int((column_widths[index_x] - image.size[0]) / 2)
pad_y = int((row_heights[index_y] - image.size[1]) / 2)
final_image.paste(image, (offset_x + pad_x, offset_y + pad_y))
offset_x += column_widths[index_x]
offset_x += args.gap
offset_y += row_heights[index_y]
offset_y += args.gap
output_file = pathclass.Path(args.output)
if output_file.extension in {'jpg', 'jpeg'}:
final_image = final_image.convert('RGB')
log.info(args.output)
final_image.save(output_file.absolute_path)
return 0
@vlogging.main_decorator
def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('image_files', nargs='+')
parser.add_argument(
'--output',
metavar='filename',
required=True,
)
parser.add_argument(
'--grid',
metavar='AxB',
help='''
Stitch the images together in grid of A columns and B rows. Your
numbers A and B should be such that A*B is larger than the number
of input images. If you add --horizontal, the images will be arranged
left-to-right first, then top-to-bottom. If you add --vertical, the
images will be arranged top-to-bottom first then left-to-right.
''',
)
parser.add_argument(
'--horizontal',
action='store_true',
help='''
Stitch the images together horizontally.
''',
)
parser.add_argument(
'--vertical',
action='store_true',
help='''
Stitch the images together vertically.
''',
)
parser.add_argument(
'--gap',
type=int,
default=0,
help='''
This many pixels of transparent gap between each row / column.
''',
)
parser.add_argument(
'--background',
type=str,
default='#00000000',
help='''
Background color as a four-channel (R, G, B, A) hex string.
This color will be seen in the --gap and behind any images that
already had transparency.
''',
)
parser.set_defaults(func=stitch_argparse)
return betterhelp.go(parser, argv)
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))