Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1078a8a9e0 | ||
|
|
e939a863c2 | ||
|
|
1d1b99ac49 | ||
|
|
c5060bafe6 | ||
|
|
055236953d | ||
|
|
346fc8590b |
@@ -31,10 +31,13 @@ Currently we just have a few nodes used by other nodes I maintain.
|
||||
- [Pad Image](docs/nodes_img.md#11-pad-image-kjset)
|
||||
- [Resize Image](docs/nodes_img.md#12-resize-image-kjset)
|
||||
- [Resize Mask](docs/nodes_img.md#13-resize-mask-kjset)
|
||||
- [Image with text label](docs/nodes_img.md#16-image-with-text-label)
|
||||
- Normalization:
|
||||
- [Normalize Image to ImageNet](docs/nodes_img.md#4-normalize-image-to-imagenet)
|
||||
- [Normalize Image to [-0.5, 0.5]](docs/nodes_img.md#5-normalize-image-to-05-05)
|
||||
- [Normalize Image to [-1, 1]](docs/nodes_img.md#6-normalize-image-to-1-1)
|
||||
- [Arbitrary Normalize](docs/nodes_img.md#14-arbitrary-normalize)
|
||||
- [Normalize Parameters](docs/nodes_img.md#15-normalize-parameters)
|
||||
- 📝 [Usage Notes](#-usage-notes)
|
||||
- 📜 [Project History](#-project-history)
|
||||
- ⚖️ [License](#️-license)
|
||||
@@ -109,6 +112,7 @@ Shows the most common uses of the resize and padding nodes. You can see how vari
|
||||
- "Approximate Fast Foreground Colour Estimation" (from [Photoroom](https://github.com/Photoroom/fast-foreground-estimation))
|
||||
- Create empty image node, from reference image
|
||||
- 1.3.0 2025-10-14: Pad Image, Resize Image and Resize Mask nodes
|
||||
- 1.4.0 2025-10-20: "Image with text label" and "Arbitrary Normalize"/"Normalize Parameters"
|
||||
|
||||
|
||||
## ⚖️ License
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
+52
-2
@@ -37,7 +37,7 @@
|
||||
- The animated frame is automatically resized to fit the bounding box dimensions before pasting. The pasting logic safely handles cases where the bbox is partially outside the image boundaries.
|
||||
|
||||
# 3. Face Composite (frame by frame)
|
||||
- **Display Name:** `"Face Composite (frame by frame)`
|
||||
- **Display Name:** `Face Composite (frame by frame)`
|
||||
- **Internal Name:** `SET_CompositeFaceFrameByFrame`
|
||||
- **Category:** `image/manipulation`
|
||||
- **Description:** This node is a simplified variant for **1-to-1** compositing. It is perfect for video processing workflows where you need to paste a sequence of processed frames back into the original video sequence at a static location.
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
|
||||
# 4. Normalize Image to ImageNet
|
||||
- **Display Name:** `"Normalize Image to ImageNet`
|
||||
- **Display Name:** `Normalize Image to ImageNet`
|
||||
- **Internal Name:** `SET_NormalizeToImageNetDataset`
|
||||
- **Category:** `image/normalization`
|
||||
- **Description:** Normalizes an image tensor using the mean and standard deviation of the ImageNet dataset.
|
||||
@@ -255,3 +255,53 @@
|
||||
- `mask` (`MASK`): The resized mask tensor.
|
||||
- `width` (`INT`): The final width of the output mask.
|
||||
- `height` (`INT`): The final height of the output mask.
|
||||
|
||||
|
||||
# 14. Arbitrary Normalize
|
||||
- **Display Name:** `Arbitrary Normalize`
|
||||
- **Internal Name:** `SET_NormalizeArbitrary`
|
||||
- **Category:** `image/normalization`
|
||||
- **Description:** Normalizes an image tensor using the mean and standard deviation provided in the `parameters` input.
|
||||
- **Purpose:** Essential for pre-processing images before feeding them into models
|
||||
- **Inputs:**
|
||||
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
|
||||
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
|
||||
- **Output:**
|
||||
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
|
||||
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the values from NORM_PARAMS
|
||||
|
||||
|
||||
# 15. Normalize Parameters
|
||||
- **Display Name:** `Normalize Parameters`
|
||||
- **Internal Name:** `SET_NormalizeParameters`
|
||||
- **Category:** `image/normalization`
|
||||
- **Description:** Provides the `NORM_PARAMS` for the [Arbitrary Normalize](#14-arbitrary-normalize) node
|
||||
- **Purpose:** This is a user interface to the [Arbitrary Normalize](#14-arbitrary-normalize) node
|
||||
- **Inputs:**
|
||||
- `mean_red` (`FLOAT`): Mean value for the red channel
|
||||
- `mean_green` (`FLOAT`): Mean value for the green channel
|
||||
- `mean_blue` (`FLOAT`): Mean value for the blue channel
|
||||
- `std_red` (`FLOAT`): Standard deviation for the red channel
|
||||
- `std_green` (`FLOAT`): Standard deviation for the green channel
|
||||
- `std_blue` (`FLOAT`): Standard deviation for the blue channel
|
||||
- **Output:**
|
||||
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
|
||||
|
||||
# 16. Image with text label
|
||||
- **Display Name:** `Image with text label`
|
||||
- **Internal Name:** `SET_ImageWithTextLabel`
|
||||
- **Category:** `image/manipulation`
|
||||
- **Description:** Adds a border and a label to an image or a mask. This can be used to create comparison grids. Use the Comfy-Core `Image Stitch` node to concatenate the images.
|
||||
- **Purpose:** Intended to create labeled images
|
||||
- **Inputs:**
|
||||
- `image` (`IMAGE`): Image to use, leave unconnected when using a mask.
|
||||
- `mask` ("MASK`): Mask to be used as image, leave unconnected when using an image.
|
||||
- `text` (`STRING`): Text to use as label. Can contain multiple lines.
|
||||
- `side` ("top", "bottom", "left", "right"): The side where the label will be placed.
|
||||
- `label_size` (`STRING`): The width (left/right) or height (top/bottom) of the area for the label. Can be expressed as a percentage (i.e. 10%) or absolute number of pixels.
|
||||
- `separation` (`STRING`): The size of the border around the image. Can be expressed as a percentage (i.e. 1%) or absolute number of pixels.
|
||||
- `background_color` (`STRING`): Color to fill the border and the label area. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
|
||||
- `foreground_color` (`STRING`): Color the label font. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
|
||||
- `font_name` (`STRING`): The name of the font to use for the text.
|
||||
- **Output:**
|
||||
- `IMAGE` (`IMAGE`): The generated RGB image.
|
||||
|
||||
+7
-2
@@ -2,14 +2,18 @@
|
||||
name = "image-misc"
|
||||
description = """
|
||||
Miscellaneous nodes for image manipulation.
|
||||
Currently just download image with bypass, so you can create workflows including image examples.
|
||||
Download image with bypass, so you can create workflows including image examples.
|
||||
Foreground estimation and mask apply.
|
||||
Face composition nodes (to reinsert a processed face).
|
||||
Advanced image/mask pad/resize (from KJNodes with some additions).
|
||||
Normalization nodes for low level image pre-processing.
|
||||
No extra dependencies, just an internal module.
|
||||
"""
|
||||
# This is the modern Python mechanism, but not supported
|
||||
# dynamic = ["version"]
|
||||
# license = "GPL-3.0-or-later"
|
||||
# Only this is supported :-(
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
license = { file = "LICENSE" }
|
||||
# The following is just ignored
|
||||
dependencies = ["seconohe"]
|
||||
@@ -20,6 +24,7 @@ Repository = "https://github.com/set-soft/ComfyUI-ImageMisc"
|
||||
[tool.comfy]
|
||||
PublisherId = "set-soft"
|
||||
DisplayName = "Image Misc"
|
||||
Icon = "https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/main/assets/Icon_400x400.jpg"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "nodes.__version__"}
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
# Project: ComfyUI-ImageMisc
|
||||
from seconohe.logger import initialize_logger
|
||||
|
||||
__version__ = "1.3.0"
|
||||
__version__ = "1.4.0"
|
||||
NODES_NAME = "ImageMisc"
|
||||
main_logger = initialize_logger(NODES_NAME)
|
||||
|
||||
+286
-6
@@ -10,12 +10,12 @@
|
||||
from copy import deepcopy
|
||||
import numpy as np
|
||||
import os
|
||||
from PIL import Image # Import the Python Imaging Library
|
||||
from PIL import Image, ImageDraw, ImageFont # Import the Python Imaging Library
|
||||
from seconohe.apply_mask import apply_mask
|
||||
from seconohe.foreground_estimation.affce import affce
|
||||
from seconohe.foreground_estimation.fmlfe import fmlfe, IMPL_PRIORITY
|
||||
from seconohe.downloader import download_file
|
||||
from seconohe.color import color_to_rgb_float
|
||||
from seconohe.color import color_to_rgb_float, color_to_rgb_uint8
|
||||
# We are the main source, so we use the main_logger
|
||||
from . import main_logger
|
||||
import torch
|
||||
@@ -89,12 +89,24 @@ PAD_TRANS = ("FLOAT", {
|
||||
"display": "number",
|
||||
"tooltip": ("The transparency for the padded area for all modes except `edge_pixel`."
|
||||
"1.0 is fully transparent, 0.0 is fully opaque.")})
|
||||
NORM_PARAM = ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.1,
|
||||
"display": "number"})
|
||||
# A dictionary to cache loaded fonts
|
||||
font_cache = {}
|
||||
|
||||
|
||||
def tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
|
||||
"""Converts a single image tensor (H, W, C) [0, 1] to a Pillow Image."""
|
||||
np_image = (tensor.cpu().numpy() * 255).astype(np.uint8)
|
||||
return Image.fromarray(np_image)
|
||||
|
||||
if tensor.dim() == 2:
|
||||
return Image.fromarray(np_image, 'L')
|
||||
else:
|
||||
return Image.fromarray(np_image, 'RGB')
|
||||
|
||||
|
||||
def pil_to_tensor(pil_image: Image.Image) -> torch.Tensor:
|
||||
@@ -108,6 +120,22 @@ def upscale(image, width, height, upscale_method):
|
||||
return common_upscale(image, width, height, upscale_method, crop="disabled")
|
||||
|
||||
|
||||
def parse_size(size_str, reference_dim):
|
||||
"""Parses a size string which can be pixels or a percentage."""
|
||||
size_str = size_str.strip()
|
||||
if size_str.endswith('%'):
|
||||
try:
|
||||
percentage = float(size_str[:-1])
|
||||
return int(reference_dim * (percentage / 100.0))
|
||||
except ValueError:
|
||||
return 0
|
||||
else:
|
||||
try:
|
||||
return int(size_str)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
if has_load_image:
|
||||
class ImageDownload:
|
||||
@classmethod
|
||||
@@ -418,7 +446,6 @@ class NormalizeToImageNetDataset():
|
||||
DESCRIPTION = ("Normalize the image to the ImageNet dataset")
|
||||
UNIQUE_NAME = "SET_NormalizeToImageNetDataset"
|
||||
DISPLAY_NAME = "Normalize Image to ImageNet"
|
||||
imagenet_normalize = None
|
||||
|
||||
def normalize(self, image: torch.Tensor):
|
||||
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
|
||||
@@ -440,7 +467,6 @@ class NormalizeToRangeMinus05to05():
|
||||
DESCRIPTION = ("Normalize the image to [-0.5, 0.5]")
|
||||
UNIQUE_NAME = "SET_NormalizeToRangeMinus05to05"
|
||||
DISPLAY_NAME = "Normalize Image to [-0.5, 0.5]"
|
||||
imagenet_normalize = None
|
||||
|
||||
def normalize(self, image: torch.Tensor):
|
||||
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
|
||||
@@ -462,7 +488,6 @@ class NormalizeToRangeMinus1to1():
|
||||
DESCRIPTION = ("Normalize the image to [-1, 1]")
|
||||
UNIQUE_NAME = "SET_NormalizeToRangeMinus1to1"
|
||||
DISPLAY_NAME = "Normalize Image to [-1, 1] (i.e. GAN)"
|
||||
imagenet_normalize = None
|
||||
|
||||
def normalize(self, image: torch.Tensor):
|
||||
return (TF.normalize(image.permute(0, 3, 1, 2), # BHWC -> BCHW
|
||||
@@ -470,6 +495,57 @@ class NormalizeToRangeMinus1to1():
|
||||
std=[0.5, 0.5, 0.5]).permute(0, 2, 3, 1),) # BCHW -> BHWC
|
||||
|
||||
|
||||
class NormalizeArbitrary():
|
||||
"""
|
||||
A ComfyUI node to normalize the values to arbitrary mean/std
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"parameters": ("NORM_PARAMS",),
|
||||
},
|
||||
}
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "normalize"
|
||||
CATEGORY = BASE_CATEGORY + "/" + NORMALIZATION
|
||||
DESCRIPTION = ("Normalize the image to the provided parameters")
|
||||
UNIQUE_NAME = "SET_NormalizeArbitrary"
|
||||
DISPLAY_NAME = "Arbitrary Normalize"
|
||||
|
||||
def normalize(self, image: torch.Tensor, parameters):
|
||||
return (TF.normalize(image.movedim(-1, 1), # BHWC -> BCHW
|
||||
mean=parameters["mean"],
|
||||
std=parameters["std"]).movedim(1, -1),) # BCHW -> BHWC
|
||||
|
||||
|
||||
class NormalizeParameters():
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mean_red": NORM_PARAM,
|
||||
"mean_green": NORM_PARAM,
|
||||
"mean_blue": NORM_PARAM,
|
||||
"std_red": NORM_PARAM,
|
||||
"std_green": NORM_PARAM,
|
||||
"std_blue": NORM_PARAM,
|
||||
},
|
||||
}
|
||||
RETURN_TYPES = ("NORM_PARAMS",)
|
||||
RETURN_NAMES = ("parameters",)
|
||||
FUNCTION = "normalize"
|
||||
CATEGORY = BASE_CATEGORY + "/" + NORMALIZATION
|
||||
DESCRIPTION = ("Parameters for the arbitrary normalization")
|
||||
UNIQUE_NAME = "SET_NormalizeParameters"
|
||||
DISPLAY_NAME = "Normalize Parameters"
|
||||
|
||||
def normalize(self, mean_red, mean_green, mean_blue, std_red, std_green, std_blue):
|
||||
return ({"mean": [mean_red, mean_green, mean_blue], "std": [std_red, std_green, std_blue]},)
|
||||
|
||||
|
||||
class ApplyMaskAFFCE:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -1245,3 +1321,207 @@ class ResizeMask:
|
||||
out_mask = common_upscale(mask.unsqueeze(1), width, height, upscale_method, crop=crop).squeeze(1)
|
||||
|
||||
return (out_mask, out_mask.shape[2], out_mask.shape[1],)
|
||||
|
||||
|
||||
# #################################################################################################
|
||||
# ImageWithTextLabel
|
||||
# #################################################################################################
|
||||
|
||||
|
||||
def load_font(font_name, font_size):
|
||||
"""Loads a font from the cache or from file."""
|
||||
if (font_name, font_size) in font_cache:
|
||||
return font_cache[(font_name, font_size)]
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype(font_name, font_size)
|
||||
except IOError:
|
||||
try:
|
||||
# Fallback for Linux
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
if not os.path.exists(font_path):
|
||||
# Fallback to a common system font if the specified one isn't found
|
||||
font_path = os.path.join("C:", os.sep, "Windows", "Fonts", f"{font_name}.ttf")
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except IOError:
|
||||
# If all else fails, use the default Pillow font
|
||||
logger.warning(f"Font '{font_name}' not found. Falling back to default font.")
|
||||
font = ImageFont.load_default()
|
||||
|
||||
font_cache[(font_name, font_size)] = font
|
||||
return font
|
||||
|
||||
|
||||
class ImageWithTextLabel:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True, "default": "Your text here",
|
||||
"tooltip": "Label for this image"}),
|
||||
"side": (["top", "bottom", "left", "right"],),
|
||||
"label_size": ("STRING", {
|
||||
"default": "10%",
|
||||
"tooltip": "Expressed as a percentage (i.e. 10%) or absolute number of pixels"}),
|
||||
"separation": ("STRING", {
|
||||
"default": "1%",
|
||||
"tooltip": "Expressed as a percentage (i.e. 1%) or absolute number of pixels"}),
|
||||
"background_color": ("STRING", {"default": "white"}),
|
||||
"foreground_color": ("STRING", {"default": "black"}),
|
||||
"font_name": ("STRING", {"default": "Arial"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE", {
|
||||
"tooltip": "Image, leave unconnected when using a mask"}),
|
||||
"mask": ("MASK", {
|
||||
"tooltip": "Mask to be used as image, leave unconnected when using an image"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "add_label"
|
||||
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
|
||||
DESCRIPTION = ("Adds a text label to an image")
|
||||
UNIQUE_NAME = "SET_ImageWithTextLabel"
|
||||
DISPLAY_NAME = "Image with text label, good for comparison grids"
|
||||
|
||||
def add_label(self, text: str, side: str, label_size: str, separation: int, background_color: str,
|
||||
foreground_color: str, font_name: str, image: Optional[torch.Tensor] = None,
|
||||
mask: Optional[torch.Tensor] = None):
|
||||
if image is None and mask is None:
|
||||
raise ValueError("You must provide an image or a mask")
|
||||
image = image if image is not None else mask
|
||||
# Process multiple images in the batch
|
||||
pil_images = [tensor_to_pil(img) for img in image]
|
||||
output_images = []
|
||||
|
||||
for pil_img in pil_images:
|
||||
original_width, original_height = pil_img.size
|
||||
|
||||
# Parse label size
|
||||
if side in ["top", "bottom"]:
|
||||
label_dim = parse_size(label_size, original_height)
|
||||
separation_px = parse_size(separation, original_height)
|
||||
else: # left, right
|
||||
label_dim = parse_size(label_size, original_width)
|
||||
separation_px = parse_size(separation, original_width)
|
||||
|
||||
# Calculate new canvas dimensions
|
||||
new_width = original_width + 2 * separation_px
|
||||
new_height = original_height + 2 * separation_px
|
||||
if side in ["top", "bottom"]:
|
||||
new_height += label_dim
|
||||
else: # left, right
|
||||
new_width += label_dim
|
||||
|
||||
# Create new image canvas
|
||||
bg_color_rgb = color_to_rgb_uint8(logger, background_color)
|
||||
canvas = Image.new("RGB", (new_width, new_height), bg_color_rgb)
|
||||
|
||||
# Paste the original image with separation
|
||||
img_paste_position = (separation_px, separation_px)
|
||||
if side == "top":
|
||||
img_paste_position = (separation_px, separation_px + label_dim)
|
||||
elif side == "left":
|
||||
img_paste_position = (separation_px + label_dim, separation_px)
|
||||
|
||||
canvas.paste(pil_img, img_paste_position)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# Define the text area
|
||||
text_area = (0, 0, 0, 0)
|
||||
if side == "top":
|
||||
text_area = (separation_px, separation_px, new_width - separation_px, separation_px + label_dim)
|
||||
elif side == "bottom":
|
||||
text_area = (separation_px, original_height + separation_px, new_width - separation_px, new_height -
|
||||
separation_px)
|
||||
elif side == "left":
|
||||
text_area = (separation_px, separation_px, separation_px + label_dim, new_height - separation_px)
|
||||
elif side == "right":
|
||||
text_area = (original_width + separation_px, separation_px, new_width - separation_px, new_height -
|
||||
separation_px)
|
||||
|
||||
text_area_width = text_area[2] - text_area[0]
|
||||
text_area_height = text_area[3] - text_area[1]
|
||||
|
||||
fg_color_rgb = color_to_rgb_uint8(logger, foreground_color)
|
||||
text_x = text_area[0] + text_area_width / 2
|
||||
text_y = text_area[1] + text_area_height / 2
|
||||
font = None
|
||||
|
||||
# --- Find best font size and render text ---
|
||||
if side in ["top", "bottom"]:
|
||||
# --- HORIZONTAL TEXT LOGIC (with wrapping) ---
|
||||
max_font_size = text_area_height
|
||||
font_size = max_font_size
|
||||
wrapped_text = text
|
||||
|
||||
while font_size > 5:
|
||||
font = load_font(font_name, font_size)
|
||||
|
||||
# Simple word wrapping
|
||||
lines = []
|
||||
words = text.split()
|
||||
current_line = ""
|
||||
for word in words:
|
||||
if font.getlength(current_line + word + " ") < text_area_width:
|
||||
current_line += word + " "
|
||||
else:
|
||||
lines.append(current_line.strip())
|
||||
current_line = word + " "
|
||||
lines.append(current_line.strip())
|
||||
wrapped_text = "\n".join(lines)
|
||||
|
||||
# Check if wrapped text fits vertically
|
||||
bbox = draw.multiline_textbbox((0, 0), wrapped_text, font=font)
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
if text_height <= text_area_height:
|
||||
break # Font size is good
|
||||
|
||||
font_size -= 1
|
||||
else:
|
||||
logger.warning("Text could not fit into the designated area even at the smallest font size.")
|
||||
|
||||
draw.multiline_text(
|
||||
(text_x, text_y),
|
||||
wrapped_text,
|
||||
fill=fg_color_rgb,
|
||||
font=font,
|
||||
anchor="mm", # middle-middle anchor
|
||||
align="center"
|
||||
)
|
||||
|
||||
else: # --- VERTICAL TEXT LOGIC ---
|
||||
max_font_size = text_area_width # For vertical, font size is constrained by width
|
||||
font_size = max_font_size
|
||||
|
||||
while font_size > 5:
|
||||
font = load_font(font_name, font_size)
|
||||
# Use textbbox with direction to measure final size
|
||||
bbox = draw.textbbox((0, 0), text, font=font, direction="ttb")
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# Check if the vertically rendered text fits in the area
|
||||
if text_width <= text_area_width and text_height <= text_area_height:
|
||||
break # Font size is good
|
||||
|
||||
font_size -= 1
|
||||
else:
|
||||
logger.warning("Text could not fit into the designated area even at the smallest font size.")
|
||||
|
||||
draw.text(
|
||||
(text_x, text_y),
|
||||
text,
|
||||
fill=fg_color_rgb,
|
||||
font=font,
|
||||
anchor="mm", # middle-middle anchor
|
||||
direction="ttb" # Top-to-bottom direction
|
||||
)
|
||||
|
||||
output_images.append(pil_to_tensor(canvas).unsqueeze(0))
|
||||
|
||||
# Stack the processed images back into a single tensor
|
||||
return (torch.cat(output_images, dim=0),)
|
||||
|
||||
Reference in New Issue
Block a user