-
Notifications
You must be signed in to change notification settings - Fork 1
/
stitch.py
139 lines (125 loc) · 4.49 KB
/
stitch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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:]))