2021-03-22 00:27:41 +00:00
|
|
|
import PIL.Image
|
|
|
|
import argparse
|
|
|
|
import sys
|
|
|
|
|
2022-02-13 03:50:00 +00:00
|
|
|
from voussoirkit import betterhelp
|
2023-12-31 22:17:00 +00:00
|
|
|
from voussoirkit import imagetools
|
2021-09-24 06:42:34 +00:00
|
|
|
from voussoirkit import pathclass
|
2021-03-22 00:27:41 +00:00
|
|
|
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)
|
2021-11-08 19:38:10 +00:00
|
|
|
files = pathclass.glob_many_files(patterns)
|
2021-09-24 06:42:34 +00:00
|
|
|
images = [PIL.Image.open(file.absolute_path) for file in files]
|
2023-12-31 22:17:00 +00:00
|
|
|
images = [imagetools.rotate_by_exif(image)[0] for image in images]
|
2021-03-22 00:27:41 +00:00
|
|
|
|
2022-04-05 17:48:08 +00:00
|
|
|
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)
|
2021-03-22 00:27:41 +00:00
|
|
|
else:
|
2022-04-05 17:48:08 +00:00
|
|
|
grid_x = len(images)
|
|
|
|
grid_y = 1
|
2021-03-22 00:27:41 +00:00
|
|
|
|
2022-04-05 17:48:08 +00:00
|
|
|
# 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
|
2021-03-22 00:27:41 +00:00
|
|
|
for image in images:
|
2022-04-05 17:48:08 +00:00
|
|
|
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
|
2021-03-22 00:27:41 +00:00
|
|
|
else:
|
2022-04-05 17:48:08 +00:00
|
|
|
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)
|
2023-09-15 04:12:21 +00:00
|
|
|
background = '#' + args.background.strip('#')
|
|
|
|
final_image = PIL.Image.new('RGBA', [final_width, final_height], color=background)
|
2022-04-05 17:48:08 +00:00
|
|
|
|
|
|
|
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
|
2021-03-22 00:27:41 +00:00
|
|
|
|
2023-12-31 22:17:00 +00:00
|
|
|
output_file = pathclass.Path(args.output)
|
|
|
|
if output_file.extension in {'jpg', 'jpeg'}:
|
|
|
|
final_image = final_image.convert('RGB')
|
|
|
|
|
2021-03-22 00:27:41 +00:00
|
|
|
log.info(args.output)
|
2023-12-31 22:17:00 +00:00
|
|
|
final_image.save(output_file.absolute_path)
|
2021-09-24 06:42:34 +00:00
|
|
|
return 0
|
2021-03-22 00:27:41 +00:00
|
|
|
|
2021-06-22 05:11:19 +00:00
|
|
|
@vlogging.main_decorator
|
2021-03-22 00:27:41 +00:00
|
|
|
def main(argv):
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
|
|
|
parser.add_argument('image_files', nargs='+')
|
2022-02-13 03:50:00 +00:00
|
|
|
parser.add_argument(
|
|
|
|
'--output',
|
2022-04-05 17:48:08 +00:00
|
|
|
metavar='filename',
|
2022-02-13 03:50:00 +00:00
|
|
|
required=True,
|
|
|
|
)
|
2022-04-05 17:48:08 +00:00
|
|
|
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.
|
|
|
|
''',
|
|
|
|
)
|
2022-02-13 03:50:00 +00:00
|
|
|
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.
|
|
|
|
''',
|
|
|
|
)
|
2023-09-15 04:12:21 +00:00
|
|
|
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.
|
|
|
|
''',
|
|
|
|
)
|
2021-03-22 00:27:41 +00:00
|
|
|
parser.set_defaults(func=stitch_argparse)
|
|
|
|
|
2022-02-13 03:50:00 +00:00
|
|
|
return betterhelp.go(parser, argv)
|
2021-03-22 00:27:41 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
raise SystemExit(main(sys.argv[1:]))
|