From f6daf4301dee9626b7d4bc9914a25e2488cbe96b Mon Sep 17 00:00:00 2001 From: drozbay <17261091+drozbay@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:20:24 -0600 Subject: [PATCH] Add position and alignment options --- comfy_extras/nodes_text_overlay.py | 54 ++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/comfy_extras/nodes_text_overlay.py b/comfy_extras/nodes_text_overlay.py index 6e5fcbf8b..4ab30e8c5 100644 --- a/comfy_extras/nodes_text_overlay.py +++ b/comfy_extras/nodes_text_overlay.py @@ -13,20 +13,23 @@ class TextOverlay(IO.ComfyNode): node_id="TextOverlay", display_name="Text Overlay", category="image/text", - description="Overlay text along the top of an image or batch of images.", + description="Overlay text on an image or batch of images.", search_aliases=["text", "label", "caption", "subtitle", "watermark", "title", "addlabel", "overlay"], inputs=[ IO.Image.Input("image"), IO.String.Input("text", multiline=True, default=""), + IO.Combo.Input("position", options=["top", "bottom"], default="top"), + IO.Combo.Input("align", options=["left", "center", "right"], default="left"), IO.Float.Input("font_size_percent", default=5.0, min=0.5, max=50.0, step=0.5, tooltip="Font size as a percentage of the image height.", advanced=True), - #IO.Combo.Input("text_color", options=["white", "black", "red", "green", "blue", "yellow", "cyan", "magenta", "gray"], default="white"), - #IO.Combo.Input("outline_color", options=["auto", "none", "black", "white", "red", "green", "blue", "yellow"], default="auto"), + #IO.Combo.Input("text_color", options=["white", "black", "red", "green", "blue", "yellow", "cyan", "magenta", "gray"], default="white", tooltip="Color of the text.", advanced=True), + #IO.Combo.Input("outline_color", options=["auto", "none", "black", "white", "red", "green", "blue", "yellow"], default="auto", tooltip="Color of the text outline.", advanced=True), + #IO.Float.Input("background_opacity", default=0.0, min=0.0, max=1.0, step=0.05, tooltip="Opacity of the background behind the text (0 = transparent, 1 = solid).", advanced=True), ], outputs=[IO.Image.Output()], ) @classmethod - def execute(cls, image, text, font_size_percent=5.0, text_color="white", outline_color="auto", margin_percent=1.0, + def execute(cls, image, text, position="top", align="left", font_size_percent=5.0, text_color="white", outline_color="auto", margin_percent=1.0, line_spacing=1.2, background_opacity=0.0, min_font_percent=2.0, outline_thickness_factor=0.04) -> IO.NodeOutput: if text.strip() == "": return IO.NodeOutput(image) @@ -49,16 +52,17 @@ class TextOverlay(IO.ComfyNode): outline_color = ImageColor.getrgb(outline_color)[:3] background_color = contrast_color if outline_color is None else outline_color - frames = [cls.render_text_on_frame(frame, text, font_size_percent, margin_percent, text_color, outline_color, background_color, + frames = [cls.render_text_on_frame(frame, text, position, align, font_size_percent, margin_percent, text_color, outline_color, background_color, line_spacing, background_opacity, min_font_percent, outline_thickness_factor) for frame in image] return IO.NodeOutput(torch.stack(frames, dim=0)) @classmethod - def render_text_on_frame(cls, frame, text, font_size_percent, margin_percent, text_color, outline_color, background_color, - line_spacing=1.2, background_opacity=0.0, min_font_percent=2.0, outline_thickness_factor=0.04, min_font_px=10): + def render_text_on_frame(cls, frame, text, position="top", align="left", font_size_percent=5.0, margin_percent=1.0, text_color="white", outline_color="auto", + background_color=None, line_spacing=1.2, background_opacity=0.0, min_font_percent=2.0, outline_thickness_factor=0.04, min_font_px=10): pil_image = PILImage.fromarray((frame.clamp(0.0, 1.0).cpu().numpy() * 255.0).astype(np.uint8), mode="RGB") width, height = pil_image.width, pil_image.height + draw = ImageDraw.Draw(pil_image) margin = int(round(margin_percent / 100.0 * min(width, height))) max_width = max(1, width - 2 * margin) @@ -67,25 +71,41 @@ class TextOverlay(IO.ComfyNode): # Font scales with resolution, then shrinks to fit the height. size = max(1, int(round(font_size_percent / 100.0 * height))) floor = min(size, max(min_font_px, int(round(min_font_percent / 100.0 * height)))) + while True: font = ImageFont.load_default(size=size) - lines = cls.wrap_text(text, font, max_width) - line_height = size * line_spacing - if line_height * len(lines) <= max_height or size <= floor: + stroke = max(1, int(round(size * outline_thickness_factor))) if outline_color is not None else 0 + block = "\n".join(cls.wrap_text(text, font, max_width)) + # convert line spacing to pixel spacing + single = draw.textbbox((0, 0), "Ay", font=font, stroke_width=stroke) + double = draw.multiline_textbbox((0, 0), "Ay\nAy", font=font, spacing=0, stroke_width=stroke) + natural_advance = (double[3] - double[1]) - (single[3] - single[1]) + pixel_spacing = int(round(size * line_spacing - natural_advance)) + box = draw.multiline_textbbox((0, 0), block, font=font, spacing=pixel_spacing, stroke_width=stroke) + block_height = box[3] - box[1] + + if block_height <= max_height or size <= floor: break + size = max(floor, int(size * 0.9)) if background_opacity > 0: - background_bottom = 2 * margin + line_height * len(lines) + band = block_height + 2 * margin + rect = [0, height - band, width, height] if position == "bottom" else [0, 0, width, band] overlay = PILImage.new("RGBA", pil_image.size, (0, 0, 0, 0)) - ImageDraw.Draw(overlay).rectangle([0, 0, width, background_bottom], fill=(*background_color, int(round(background_opacity * 255)))) + ImageDraw.Draw(overlay).rectangle(rect, fill=(*background_color, int(round(background_opacity * 255)))) pil_image = PILImage.alpha_composite(pil_image.convert("RGBA"), overlay).convert("RGB") + draw = ImageDraw.Draw(pil_image) - draw = ImageDraw.Draw(pil_image) - stroke = max(1, int(round(size * outline_thickness_factor))) if outline_color is not None else 0 - for index, line in enumerate(lines): - draw.text((margin, margin + index * line_height), line, font=font, - fill=text_color, stroke_width=stroke, stroke_fill=outline_color) + anchor_h, x = {"left": ("l", margin), "center": ("m", width / 2), "right": ("r", width - margin)}[align] + + if position == "bottom": + anchor_v, y = "d", height - margin + else: + anchor_v, y = "a", margin + + draw.multiline_text((x, y), block, font=font, fill=text_color, anchor=anchor_h + anchor_v, + align=align, spacing=pixel_spacing, stroke_width=stroke, stroke_fill=outline_color) return torch.from_numpy(np.array(pil_image).astype(np.float32) / 255.0)