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])) | |