99from PIL import Image , ImageDraw , ImageFont
1010import math
1111
12- DATE_REGEX_1 = re .compile (r"(\d{4}-\d{2}-\d{2})" )
13- DATE_REGEX_2 = re .compile (r"(\d{4}\d{2}\d{2})" )
12+ DATE_REGEX_1 = re .compile (r"(\d{4}\d{2}\d{2}\d{2}\d{2}\d{2})" )
13+ DATE_REGEX_2 = re .compile (r"(\d{4}-\d{2}-\d{2})" )
14+ DATE_REGEX_3 = re .compile (r"(\d{4}\d{2}\d{2})" )
1415
1516
1617def parse_args ():
@@ -34,6 +35,29 @@ def parse_args():
3435 type = int ,
3536 help = "Font size for the title (in pixels). Defaults to ~3%% of image width." ,
3637 )
38+ parser .add_argument (
39+ "--coltitles" ,
40+ help = "Optional title to place at the top of every column. Separate titles should be comma-separated." ,
41+ )
42+ parser .add_argument (
43+ "--coltitles-size" ,
44+ type = int ,
45+ help = "Font size for the column titles (in pixels). Defaults to ~2%% of image width." ,
46+ )
47+ parser .add_argument (
48+ "--rowtitles" ,
49+ help = "Optional title to place at the top of every row. Separate titles should be comma-separated." ,
50+ )
51+ parser .add_argument (
52+ "--rowtitles-size" ,
53+ type = int ,
54+ help = "Font size for the row titles (in pixels). Defaults to ~2%% of image width." ,
55+ )
56+ parser .add_argument (
57+ "--resize" ,
58+ type = int ,
59+ help = "Resize factor of the final image (in percent), default is 100%" ,
60+ )
3761 parser .add_argument (
3862 "--output" ,
3963 default = "composite.jpg" ,
@@ -48,6 +72,9 @@ def extract_date_from_filename(filename):
4872 if match :
4973 return match .group (1 )
5074 match = DATE_REGEX_2 .search (filename )
75+ if match :
76+ return match .group (1 )
77+ match = DATE_REGEX_3 .search (filename )
5178 if match :
5279 return match .group (1 )
5380 return "unknown"
@@ -69,56 +96,106 @@ def load_images(image_paths):
6996 return [Image .open (p ).convert ("RGB" ) for p in image_paths ]
7097
7198
72- def create_composite (image_groups , date_order , title = None , title_size = None ):
73- from PIL import ImageFont
74-
99+ def create_composite (
100+ image_groups ,
101+ date_order ,
102+ title = None ,
103+ title_size = None ,
104+ col_titles = None ,
105+ coltitle_size = None ,
106+ row_titles = None ,
107+ rowtitle_size = None ,
108+ resize_percent = 100 ,
109+ ):
75110 date_keys = sorted (image_groups )
76111 grouped_images = [load_images (image_groups [date ]) for date in date_keys ]
77112
78113 max_w = max (img .width for group in grouped_images for img in group )
79114 max_h = max (img .height for group in grouped_images for img in group )
80115 pad = 10
81116 pad_title = 20
82-
117+ # Determine layout
83118 if date_order == "row" :
84119 rows = len (grouped_images )
85120 cols = max (len (group ) for group in grouped_images )
86121 else :
87122 cols = len (grouped_images )
88123 rows = max (len (group ) for group in grouped_images )
89124
125+ # Font setup
90126 canvas_w = cols * (max_w + pad ) + pad
91-
92- # Choose title font size
93- auto_font_size = max (16 , int (canvas_w * 0.02 )) # 3% of width or minimum 16px
94- font_size = title_size if title_size else auto_font_size
95-
127+ auto_font_size = max (16 , int (canvas_w * 0.03 ))
128+ font_size = int (title_size ) if title_size else auto_font_size
96129 font = ImageFont .load_default (font_size )
97130
98- title_h = font_size + pad_title if title else 0
99- canvas_h = rows * (max_h + pad ) + pad + title_h
131+ auto_font_size = max (14 , int (canvas_w * 0.02 ))
132+ coltitle_size = int (coltitle_size ) if coltitle_size else auto_font_size
133+ rowtitle_size = int (rowtitle_size ) if rowtitle_size else auto_font_size
134+ rowfont = ImageFont .load_default (rowtitle_size )
135+ colfont = ImageFont .load_default (coltitle_size )
136+
137+ draw_dummy = ImageDraw .Draw (Image .new ("RGB" , (1 , 1 )))
138+ row_title_width = (
139+ max (draw_dummy .textlength (title , font = rowfont ) for title in row_titles ) + pad
140+ if row_titles
141+ else 0
142+ )
143+ col_title_height = coltitle_size + pad_title if col_titles else 0
144+ main_title_height = font_size + pad_title if title else 0
145+
146+ # Canvas dimensions
147+ canvas_w += row_title_width
148+ canvas_w = int (canvas_w )
149+ canvas_h = int (rows * (max_h + pad ) + pad + main_title_height + col_title_height )
100150
101151 composite = Image .new ("RGB" , (canvas_w , canvas_h ), color = "white" )
102152 draw = ImageDraw .Draw (composite )
103153
104- # Draw title centered
154+ # Draw main title
105155 if title :
106156 text_w = draw .textlength (title , font = font )
107157 draw .text (((canvas_w - text_w ) // 2 , pad ), title , font = font , fill = "black" )
108158
109- y_offset = title_h
159+ y_offset = main_title_height + col_title_height
110160
111161 for i , (date , images ) in enumerate (zip (date_keys , grouped_images )):
112162 for j , img in enumerate (images ):
113163 if date_order == "row" :
114- x = pad + j * (max_w + pad )
115- y = y_offset + pad + i * (max_h + pad )
164+ row_idx , col_idx = i , j
116165 else :
117- x = pad + i * (max_w + pad )
118- y = y_offset + pad + j * (max_h + pad )
166+ row_idx , col_idx = j , i
167+
168+ x = int (pad + row_title_width + col_idx * (max_w + pad ))
169+ y = int (y_offset + row_idx * (max_h + pad ))
119170
120171 composite .paste (img .resize ((max_w , max_h )), (x , y ))
121172
173+ # Draw column titles
174+ if col_titles :
175+ for j in range (cols ):
176+ col_title = col_titles [j ] if j < len (col_titles ) else ""
177+ x = pad + row_title_width + j * (max_w + pad )
178+ y = main_title_height
179+ draw .text (
180+ (x + max_w // 2 - draw .textlength (col_title , font = font ) // 2 , y ),
181+ col_title ,
182+ font = colfont ,
183+ fill = "black" ,
184+ )
185+
186+ # Draw row titles
187+ if row_titles :
188+ for i in range (rows ):
189+ row_title = row_titles [i ] if i < len (row_titles ) else ""
190+ x = pad
191+ y = y_offset + i * (max_h + pad ) + max_h // 2 - font_size // 2
192+ draw .text ((x , y ), row_title , font = rowfont , fill = "black" )
193+ # Resize if needed
194+ if resize_percent != 100 :
195+ new_w = int (composite .width * resize_percent / 100 )
196+ new_h = int (composite .height * resize_percent / 100 )
197+ composite = composite .resize ((new_w , new_h ))
198+
122199 return composite
123200
124201
@@ -130,8 +207,21 @@ def main():
130207 print ("No images found." )
131208 return
132209
210+ if args .rowtitles :
211+ args .rowtitles = args .rowtitles .split ("," )
212+ if args .coltitles :
213+ args .coltitles = args .coltitles .split ("," )
214+
133215 composite = create_composite (
134- image_groups , args .date_order , title = args .title , title_size = args .title_size
216+ image_groups ,
217+ args .date_order ,
218+ title = args .title ,
219+ title_size = args .title_size ,
220+ row_titles = args .rowtitles ,
221+ rowtitle_size = args .rowtitles_size ,
222+ col_titles = args .coltitles ,
223+ coltitle_size = args .coltitles_size ,
224+ resize_percent = args .resize ,
135225 )
136226 composite .save (args .output )
137227 print (f"Composite image saved to { args .output } " )
0 commit comments