Initial commit.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
fonts/*
|
||||
@@ -0,0 +1,50 @@
|
||||
# TextOverlay Node for ComfyUI
|
||||
|
||||
The TextOverlay node allows users to overlay text on images. The node supports adjustments for font size, type, color, alignment, and position, making it versatile for various applications.
|
||||
|
||||
Originally inspired by the [Text Overlay Plugin by mikkel](https://github.com/mikkel/ComfyUI-text-overlay), this node has been rebuilt and expanded to include additional features and improvements.
|
||||
|
||||
## Features
|
||||
|
||||
- **Font Customization**: Choose from different font sizes and types to match your design.
|
||||
- **Color Selection**: Specify text and stroke colors in hex format for perfect matching.
|
||||
- **Alignment and Positioning**: Align your text horizontally and vertically, with options for precise position adjustments.
|
||||
- **Stroke Customization**: Add a stroke to your text with customizable color and thickness.
|
||||
- **Supports Single and Batch Processing**: Process single images or multiple images in a batch.
|
||||
|
||||
## Installation
|
||||
|
||||
To use the TextOverlay node, ensure this repo is included in your ComfyUI `custom_nodes` directory.
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, the TextOverlay node will be available in the ComfyUI interface under the `image/text` category. To use the node:
|
||||
|
||||
1. Add the TextOverlay node to your workflow.
|
||||
2. Connect an image source to the node's image input.
|
||||
3. Configure the node parameters in the UI, including text content, font specifications, color, alignment, and positioning adjustments.
|
||||
4. Connect the node's output to your desired destination in the workflow.
|
||||
|
||||
### Custom Fonts
|
||||
|
||||
To use a custom font, ensure the font file is included in the `fonts` directory within the `custom_nodes` directory. Then, specify the font file name (e.g., `arial.ttf`) in the `font` parameter of the TextOverlay node. If using a system-installed font, you can specify the font name directly without the file path. If the font file is not found, the node will default to a system-installed font.
|
||||
|
||||
## Input Parameters
|
||||
|
||||
The TextOverlay node requires the following inputs:
|
||||
|
||||
- `image`: The input image(s) to overlay text on.
|
||||
- `text`: The text content to overlay.
|
||||
- `font_size`: Size of the font.
|
||||
- `font`: Name of the font file (e.g., `arial.ttf`).
|
||||
- `fill_color_hex`: Hex code for the text color.
|
||||
- `stroke_color_hex`: Hex code for the text stroke color.
|
||||
- `stroke_thickness`: Thickness of the stroke.
|
||||
- `padding`: Padding around the text.
|
||||
- `horizontal_alignment`: Horizontal text alignment (`left`, `center`, `right`).
|
||||
- `vertical_alignment`: Vertical text alignment (`top`, `middle`, `bottom`).
|
||||
- `x_shift`, `y_shift`: Adjustments for text positioning.
|
||||
|
||||
## Output
|
||||
|
||||
The node overlays the specified text according to the configured parameters and output the modified input image(s).
|
||||
@@ -0,0 +1,3 @@
|
||||
from .nodes import NODE_CLASS_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS"]
|
||||
@@ -0,0 +1,314 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
class TextOverlay:
|
||||
"""
|
||||
A node to overlay text on images with various customization options such as
|
||||
font size, font type, text color, stroke color, stroke thickness, padding,
|
||||
alignment, and position adjustments.
|
||||
"""
|
||||
|
||||
_horizontal_alignments = ["left", "center", "right"]
|
||||
_vertical_alignments = ["top", "middle", "bottom"]
|
||||
|
||||
def __init__(self, device="cpu"):
|
||||
"""
|
||||
Initializes the TextOverlay node.
|
||||
|
||||
Parameters:
|
||||
- device (str): The computing device to use, default is 'cpu'.
|
||||
"""
|
||||
|
||||
self.device = device
|
||||
self._loaded_font = None
|
||||
self._full_text = None
|
||||
self._x = None
|
||||
self._y = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""
|
||||
Defines the expected input parameters for the text overlay functionality.
|
||||
|
||||
Returns:
|
||||
- dict: A dictionary specifying required inputs and their attributes.
|
||||
"""
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",), # Input image to overlay text on
|
||||
"text": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": "Hello"},
|
||||
), # Text to overlay
|
||||
"font_size": (
|
||||
"INT",
|
||||
{"default": 32, "min": 1, "max": 256, "step": 1},
|
||||
), # Font size
|
||||
"font": (
|
||||
"STRING",
|
||||
{"default": "arial.ttf"},
|
||||
), # Font name (e.g. arial.ttf)
|
||||
"fill_color_hex": (
|
||||
"STRING",
|
||||
{"default": "#FFFFFF"},
|
||||
), # Text fill color in hex
|
||||
"stroke_color_hex": (
|
||||
"STRING",
|
||||
{"default": "#000000"},
|
||||
), # Text stroke color in hex
|
||||
"stroke_thickness": (
|
||||
"FLOAT",
|
||||
{"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01},
|
||||
), # Stroke thickness
|
||||
"padding": (
|
||||
"INT",
|
||||
{"default": 16, "min": 0, "max": 128, "step": 1},
|
||||
), # Padding around text
|
||||
"horizontal_alignment": (
|
||||
cls._horizontal_alignments,
|
||||
{"default": "center"},
|
||||
), # Horizontal alignment
|
||||
"vertical_alignment": (
|
||||
cls._vertical_alignments,
|
||||
{"default": "bottom"},
|
||||
), # Vertical alignment
|
||||
"x_shift": (
|
||||
"INT",
|
||||
{"default": 0, "min": -128, "max": 128, "step": 1},
|
||||
), # Horizontal position adjustment
|
||||
"y_shift": (
|
||||
"INT",
|
||||
{"default": 0, "min": -128, "max": 128, "step": 1},
|
||||
), # Vertical position adjustment
|
||||
}
|
||||
}
|
||||
|
||||
# Static attributes defining the return type, function name, and category for the class
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "batch_process"
|
||||
CATEGORY = "image/text"
|
||||
|
||||
def hex_to_rgb(self, hex_color):
|
||||
"""
|
||||
Converts a hex color string to an RGB tuple.
|
||||
|
||||
Parameters:
|
||||
- hex_color (str): The color in hex format as a string.
|
||||
|
||||
Returns:
|
||||
- tuple: The color converted to an (R, G, B) tuple.
|
||||
"""
|
||||
|
||||
hex_color = hex_color.lstrip("#")
|
||||
if len(hex_color) == 3:
|
||||
hex_color = hex_color * 2
|
||||
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
def draw_text(
|
||||
self,
|
||||
image,
|
||||
text,
|
||||
font_size,
|
||||
font,
|
||||
fill_color_hex,
|
||||
stroke_color_hex,
|
||||
stroke_thickness,
|
||||
padding,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
x_shift,
|
||||
y_shift,
|
||||
use_cache=False,
|
||||
):
|
||||
"""
|
||||
Draws the specified text on the given image with the provided styling and alignment options.
|
||||
|
||||
Parameters:
|
||||
- image (PIL.Image.Image): The image to draw text on.
|
||||
- text (str): The text to overlay on the image.
|
||||
- font_size (int): The font size of the text.
|
||||
- font (str): The font type of the text.
|
||||
- fill_color_hex (str): The fill color of the text in hex format.
|
||||
- stroke_color_hex (str): The stroke color of the text in hex format.
|
||||
- stroke_thickness (float): The thickness of the text stroke.
|
||||
- padding (int): The padding around the text.
|
||||
- horizontal_alignment (str): The horizontal alignment of the text ('left', 'center', 'right').
|
||||
- vertical_alignment (str): The vertical alignment of the text ('top', 'middle', 'bottom').
|
||||
- x_shift (int): Horizontal position adjustment for the text.
|
||||
- y_shift (int): Vertical position adjustment for the text.
|
||||
- use_cache (bool): Flag to use cached font and position calculations to improve performance.
|
||||
|
||||
Returns:
|
||||
- PIL.Image.Image: The image with the text overlay applied.
|
||||
"""
|
||||
|
||||
# Load font from the fonts directory or use default if not found or specified to not use cache
|
||||
if self._loaded_font is None or use_cache is False:
|
||||
fonts_dir = os.path.join(os.path.dirname(__file__), "fonts")
|
||||
font_path = os.path.join(fonts_dir, font)
|
||||
|
||||
# Check if the font file exists in the fonts directory
|
||||
if not os.path.exists(font_path):
|
||||
# If not, set path to font name directly - this will cause PIL to search for the font in the system
|
||||
font_path = font
|
||||
|
||||
try:
|
||||
self._loaded_font = ImageFont.truetype(font_path, font_size)
|
||||
except Exception as e:
|
||||
print(f"Error loading font: {e}... Using default font")
|
||||
self._loaded_font = ImageFont.load_default()
|
||||
|
||||
# Prepare to draw on the image
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Process text for multiline support and fit within image dimensions
|
||||
words = text.replace("\n", "\n ").split(" ")
|
||||
if self._full_text is None or use_cache is False:
|
||||
text_lines, line = [], ""
|
||||
for word in words:
|
||||
extra_line = "\n" in word
|
||||
word = word.strip()
|
||||
if (
|
||||
draw.textlength(line + word, font=self._loaded_font)
|
||||
< image.width - 2 * padding
|
||||
):
|
||||
line += word + " "
|
||||
else:
|
||||
text_lines.append(line.strip())
|
||||
line = word + " "
|
||||
if extra_line:
|
||||
text_lines.append(line.strip())
|
||||
line = ""
|
||||
text_lines.append(line.strip())
|
||||
self._full_text = "\n".join(text_lines)
|
||||
|
||||
# Calculate text position based on alignment and position adjustments
|
||||
if self._x is None or self._y is None or use_cache is False:
|
||||
left, top, right, bottom = draw.multiline_textbbox(
|
||||
(0, 0),
|
||||
self._full_text,
|
||||
font=self._loaded_font,
|
||||
stroke_width=int(font_size * stroke_thickness * 0.5),
|
||||
align=horizontal_alignment,
|
||||
)
|
||||
if horizontal_alignment == "left":
|
||||
self._x = padding
|
||||
elif horizontal_alignment == "center":
|
||||
self._x = (image.width - (right - left)) / 2
|
||||
elif horizontal_alignment == "right":
|
||||
self._x = image.width - (right - left) - padding
|
||||
self._x += x_shift
|
||||
if vertical_alignment == "middle":
|
||||
self._y = (image.height - (bottom - top)) / 2
|
||||
elif vertical_alignment == "top":
|
||||
self._y = padding
|
||||
elif vertical_alignment == "bottom":
|
||||
self._y = image.height - (bottom - top) - padding
|
||||
self._y += y_shift
|
||||
|
||||
# Draw the processed text onto the image
|
||||
draw.text(
|
||||
(self._x, self._y),
|
||||
self._full_text,
|
||||
fill=self.hex_to_rgb(fill_color_hex),
|
||||
stroke_fill=self.hex_to_rgb(stroke_color_hex),
|
||||
stroke_width=int(font_size * stroke_thickness * 0.5),
|
||||
font=self._loaded_font,
|
||||
align=horizontal_alignment,
|
||||
)
|
||||
return image
|
||||
|
||||
def batch_process(
|
||||
self,
|
||||
image,
|
||||
text,
|
||||
font_size,
|
||||
font,
|
||||
fill_color_hex,
|
||||
stroke_color_hex,
|
||||
stroke_thickness,
|
||||
padding,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
x_shift,
|
||||
y_shift,
|
||||
):
|
||||
"""
|
||||
Processes a batch of images or a single image, adding the specified text overlay
|
||||
with the given styling and alignment options.
|
||||
|
||||
Parameters:
|
||||
- image (torch.Tensor or numpy.ndarray): The image(s) to process.
|
||||
- text (str): The text to overlay on the image(s).
|
||||
- font_size (int): The font size of the text.
|
||||
- font (str): The font type of the text.
|
||||
- fill_color_hex (str): The fill color of the text in hex format.
|
||||
- stroke_color_hex (str): The stroke color of the text in hex format.
|
||||
- stroke_thickness (float): The thickness of the text stroke.
|
||||
- padding (int): The padding around the text.
|
||||
- horizontal_alignment (str): The horizontal alignment of the text.
|
||||
- vertical_alignment (str): The vertical alignment of the text.
|
||||
- x_shift (int): Horizontal position adjustment for the text.
|
||||
- y_shift (int): Vertical position adjustment for the text.
|
||||
|
||||
Returns:
|
||||
- tuple: A tuple containing the processed image(s) as a torch.Tensor.
|
||||
"""
|
||||
|
||||
# Handles both single and batch image processing for text overlay
|
||||
if len(image.shape) == 3: # Single image
|
||||
image_np = image.cpu().numpy()
|
||||
image = Image.fromarray((image_np.squeeze(0) * 255).astype(np.uint8))
|
||||
image = self.draw_text(
|
||||
image,
|
||||
text,
|
||||
font_size,
|
||||
font,
|
||||
fill_color_hex,
|
||||
stroke_color_hex,
|
||||
stroke_thickness,
|
||||
padding,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
x_shift,
|
||||
y_shift,
|
||||
)
|
||||
image_tensor_out = torch.tensor(np.array(image).astype(np.float32) / 255.0)
|
||||
image_tensor_out = torch.unsqueeze(image_tensor_out, 0)
|
||||
return (image_tensor_out,)
|
||||
else: # Batch of images
|
||||
image_np = image.cpu().numpy()
|
||||
images = [Image.fromarray((img * 255).astype(np.uint8)) for img in image_np]
|
||||
images_out, use_cache = [], False
|
||||
for img in images:
|
||||
img = self.draw_text(
|
||||
img,
|
||||
text,
|
||||
font_size,
|
||||
font,
|
||||
fill_color_hex,
|
||||
stroke_color_hex,
|
||||
stroke_thickness,
|
||||
padding,
|
||||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
x_shift,
|
||||
y_shift,
|
||||
use_cache,
|
||||
)
|
||||
images_out.append(np.array(img).astype(np.float32) / 255.0)
|
||||
use_cache = True
|
||||
images_np = np.stack(images_out)
|
||||
images_tensor = torch.from_numpy(images_np)
|
||||
return (images_tensor,)
|
||||
|
||||
|
||||
# Mapping of node class names to their respective classes
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Text Overlay": TextOverlay,
|
||||
}
|
||||
Reference in New Issue
Block a user