Spaces:
Running
on
Zero
Running
on
Zero
#---------------------------------------------------------------------------------------------------------------------# | |
# Comfyroll Studio custom nodes by RockOfFire and Akatsuzi https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes | |
# for ComfyUI https://github.com/comfyanonymous/ComfyUI | |
#---------------------------------------------------------------------------------------------------------------------# | |
# these functions are a straight clone from | |
# https://github.com/LEv145/images-grid-comfy-plugin | |
import typing as t | |
from dataclasses import dataclass | |
from contextlib import suppress | |
from PIL import Image, ImageDraw, ImageFont | |
WIDEST_LETTER = "W" | |
class Annotation(): | |
column_texts: list[str] | |
row_texts: list[str] | |
font: ImageFont.FreeTypeFont | |
def create_images_grid_by_columns( | |
images: list[Image.Image], | |
gap: int, | |
max_columns: int, | |
annotation: t.Optional[Annotation] = None, | |
) -> Image.Image: | |
max_rows = (len(images) + max_columns - 1) // max_columns | |
return _create_images_grid(images, gap, max_columns, max_rows, annotation) | |
def create_images_grid_by_rows( | |
images: list[Image.Image], | |
gap: int, | |
max_rows: int, | |
annotation: t.Optional[Annotation] = None, | |
) -> Image.Image: | |
max_columns = (len(images) + max_rows - 1) // max_rows | |
return _create_images_grid(images, gap, max_columns, max_rows, annotation) | |
class _GridInfo(): | |
image: Image.Image | |
gap: int | |
one_image_size: tuple[int, int] | |
def _create_images_grid( | |
images: list[Image.Image], | |
gap: int, | |
max_columns: int, | |
max_rows: int, | |
annotation: t.Optional[Annotation], | |
) -> Image.Image: | |
size = images[0].size | |
grid_width = size[0] * max_columns + (max_columns - 1) * gap | |
grid_height = size[1] * max_rows + (max_rows - 1) * gap | |
grid_image = Image.new("RGB", (grid_width, grid_height), color="white") | |
_arrange_images_on_grid(grid_image, images=images, size=size, max_columns=max_columns, gap=gap) | |
if annotation is None: | |
return grid_image | |
return _create_grid_annotation( | |
grid_info=_GridInfo( | |
image=grid_image, | |
gap=gap, | |
one_image_size=size, | |
), | |
column_texts=annotation.column_texts, | |
row_texts=annotation.row_texts, | |
font=annotation.font, | |
) | |
def _arrange_images_on_grid( | |
grid_image: Image.Image, | |
/, | |
images: list[Image.Image], | |
size: tuple[int, int], | |
max_columns: int, | |
gap: int, | |
): | |
for i, image in enumerate(images): | |
x = (i % max_columns) * (size[0] + gap) | |
y = (i // max_columns) * (size[1] + gap) | |
grid_image.paste(image, (x, y)) | |
def _create_grid_annotation( | |
grid_info: _GridInfo, | |
column_texts: list[str], | |
row_texts: list[str], | |
font: ImageFont.FreeTypeFont, | |
) -> Image.Image: | |
if not column_texts and not row_texts: | |
raise ValueError("Column text and row text is empty") | |
grid = grid_info.image | |
left_padding = 0 | |
top_padding = 0 | |
if row_texts: | |
left_padding = int( | |
max( | |
font.getlength(splitted_text) | |
for raw_text in row_texts | |
for splitted_text in raw_text.split("\n") | |
) | |
+ font.getlength(WIDEST_LETTER)*2 | |
) | |
if column_texts: | |
top_padding = int(font.size * 2) | |
image = Image.new( | |
"RGB", | |
(grid.size[0] + left_padding, grid.size[1] + top_padding), | |
color="white", | |
) | |
draw = ImageDraw.Draw(image) | |
# https://github.com/python-pillow/Pillow/blob/9.5.x/docs/reference/ImageDraw.rst | |
draw.font = font # type: ignore | |
_paste_image_to_lower_left_corner(image, grid) | |
if column_texts: | |
_draw_column_text( | |
draw=draw, | |
texts=column_texts, | |
grid_info=grid_info, | |
left_padding=left_padding, | |
top_padding=top_padding, | |
) | |
if row_texts: | |
_draw_row_text( | |
draw=draw, | |
texts=row_texts, | |
grid_info=grid_info, | |
left_padding=left_padding, | |
top_padding=top_padding, | |
) | |
return image | |
def _draw_column_text( | |
draw: ImageDraw.ImageDraw, | |
texts: list[str], | |
grid_info: _GridInfo, | |
left_padding: int, | |
top_padding: int, | |
) -> None: | |
i = 0 | |
x0 = left_padding | |
y0 = 0 | |
x1 = left_padding + grid_info.one_image_size[0] | |
y1 = top_padding | |
while x0 != grid_info.image.size[0] + left_padding + grid_info.gap: | |
i = _draw_text_by_xy((x0, y0, x1, y1), i, draw=draw, texts=texts) | |
x0 += grid_info.one_image_size[0] + grid_info.gap | |
x1 += grid_info.one_image_size[0] + grid_info.gap | |
def _draw_row_text( | |
draw: ImageDraw.ImageDraw, | |
texts: list[str], | |
grid_info: _GridInfo, | |
left_padding: int, | |
top_padding: int, | |
) -> None: | |
i = 0 | |
x0 = 0 | |
y0 = top_padding | |
x1 = left_padding | |
y1 = top_padding + grid_info.one_image_size[1] | |
while y0 != grid_info.image.size[1] + top_padding + grid_info.gap: | |
i = _draw_text_by_xy((x0, y0, x1, y1), i, draw=draw, texts=texts) | |
y0 += grid_info.one_image_size[1] + grid_info.gap | |
y1 += grid_info.one_image_size[1] + grid_info.gap | |
def _draw_text_by_xy( | |
xy: tuple[int, int, int, int], | |
index: int, | |
\ | |
draw: ImageDraw.ImageDraw, | |
texts: list[str], | |
) -> int: | |
with suppress(IndexError): | |
_draw_center_text(draw, xy, texts[index]) | |
return index + 1 | |
def _draw_center_text( | |
draw: ImageDraw.ImageDraw, | |
xy: tuple[int, int, int, int], | |
text: str, | |
fill: t.Any = "black", | |
) -> None: | |
_, _, *text_size = draw.textbbox((0, 0), text) | |
draw.multiline_text( | |
( | |
(xy[2] - text_size[0] + xy[0]) / 2, | |
(xy[3] - text_size[1] + xy[1]) / 2, | |
), | |
text, | |
fill=fill, | |
) | |
def _paste_image_to_lower_left_corner(base: Image.Image, image: Image.Image) -> None: | |
base.paste(image, (base.size[0] - image.size[0], base.size[1] - image.size[1])) | |