15 Commits
Author SHA1 Message Date
scofano d3d9bc6cff Fix image markdown syntax in README.md 2025-10-28 11:31:37 -03:00
scofano 23a4bb6a79 Update screenshot to be a clickable link 2025-10-28 11:31:04 -03:00
scofano bcfb465380 Added Font Alignment 2025-10-28 11:27:10 -03:00
scofano bdb7ce1d85 Merge branch 'main' of https://github.com/scofano/ComfyUI-Advanced-TextOverlay 2025-10-28 10:24:33 -03:00
scofano 673e9833eb Added Letter Spacing 2025-10-28 10:24:17 -03:00
scofano 923879040a Add letter spacing parameter to README 2025-10-28 10:23:28 -03:00
scofano bc5428af5d Added new Parameter - Letter Spacing 2025-10-28 10:21:45 -03:00
scofano ba0a48677e new ranges 2025-10-24 15:26:50 -03:00
scofano 830be708b6 Fix formatting and update key improvements section 2025-10-24 14:20:56 -03:00
scofano 7112fd3f09 Update image link in README.md 2025-10-24 14:20:37 -03:00
scofano 5a49ca6ef1 Update image link to use raw GitHub URL 2025-10-24 14:19:36 -03:00
scofano 40d045ec35 Add screenshot to README for text overlay
Added a screenshot to the README for better visualization.
2025-10-24 14:18:53 -03:00
scofano 9c74a78c98 updates 2025-10-24 14:16:34 -03:00
scofano dedd7b88f5 update 2025-10-24 13:58:11 -03:00
scofano 62617e530a Update README.md 2025-10-24 13:50:19 -03:00
5 changed files with 127 additions and 60 deletions
+7 -2
View File
@@ -1,12 +1,16 @@
# 🖋️ ComfyUI Advanced Text Overlay
**Repository:** [scofano/ComfyUI-Advanced-TextOverlay](https://github.com/scofano/ComfyUI-Advanced-TextOverlay)
**Forked from:** [munkyfoot/ComfyUI-TextOverlay](https://github.com/munkyfoot/ComfyUI-TextOverlay)
**Originally inspired by:** [mikkel/ComfyUI-text-overlay](https://github.com/mikkel/ComfyUI-text-overlay)
This node extends and refines the classic ComfyUI Text Overlay concept with a **re-engineered rendering engine**, **precise alignment logic**, **new customization parameters**, and **enhanced batch handling** — making it the most advanced text overlay solution available for ComfyUI.
---
![Advanced Text Overlay Screenshot](https://github.com/scofano/ComfyUI-Advanced-TextOverlay/blob/main/animated.gif)
## ✨ Key Improvements & New Features
@@ -18,7 +22,7 @@ This node extends and refines the classic ComfyUI Text Overlay concept with a **
### 🎨 2. Extended Styling and Layout Control
* **Line spacing** and **padding** parameters for advanced multi-line composition.
* **Line spacing**, **letter spacing** and **padding** parameters for advanced multi-line composition.
* **Independent X/Y shifting** for fine-tuned manual positioning.
* **Full hex color control** for both text fill and stroke.
* Supports transparent overlays for non-destructive compositing.
@@ -102,6 +106,7 @@ To install the **Advanced Text Overlay** node:
| **x_shift** | Float | Horizontal offset adjustment (pixels). |
| **y_shift** | Float | Vertical offset adjustment (pixels). |
| **line_spacing** | Float | Vertical spacing between lines of text. |
| **letter_spacing** | Float | Horizontal spacing between letters. |
---
@@ -115,4 +120,4 @@ The node outputs the modified image (or batch of images) with the specified text
* Original concept by [mikkel](https://github.com/mikkel/ComfyUI-text-overlay)
* Fork base by [munkyfoot](https://github.com/munkyfoot/ComfyUI-TextOverlay)
* Advanced reimplementation and feature expansion by [scofano](https://github.com/scofano)
* Advanced reimplementation and feature expansion by [scofano](https://github.com/scofano)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

+104 -58
View File
@@ -38,15 +38,17 @@ class TextOverlay:
# font, font-size, font color, font alpha
"font": ("STRING", {"default": "ariblk.ttf"}),
"font_size": ("INT", {"default": 32, "min": 1, "max": 9999, "step": 1}),
"letter_spacing": ("FLOAT", {"default": 0.0, "min": -10.0, "max": 50.0, "step": 0.5}),
"font_alignment": (cls._horizontal_alignments, {"default": "center"}),
"fill_color_hex": ("STRING", {"default": "#FFFFFF"}),
"fill_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
# padding, hor_align, x_shift, vert align, y_shift, line_spacing
"padding": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}),
"padding": ("INT", {"default": 16, "min": 0, "max": 1024, "step": 1}),
"vertical_alignment": (cls._vertical_alignments, {"default": "middle"}),
"y_shift": ("INT", {"default": 0, "min": -256, "max": 256, "step": 1}),
"y_shift": ("INT", {"default": 0, "min": -1024, "max": 1024, "step": 1}),
"horizontal_alignment": (cls._horizontal_alignments, {"default": "center"}),
"x_shift": ("INT", {"default": 0, "min": -256, "max": 256, "step": 1}),
"x_shift": ("INT", {"default": 0, "min": -1024, "max": 1024, "step": 1}),
"line_spacing": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 50.0, "step": 0.5}),
# all strokes
@@ -57,14 +59,14 @@ class TextOverlay:
# all boxes
"bg_enable": ("BOOLEAN", {"default": False}),
"bg_padding": ("INT", {"default": 8, "min": 0, "max": 256, "step": 1}),
"bg_padding": ("INT", {"default": 8, "min": 0, "max": 1024, "step": 1}),
"bg_radius": ("INT", {"default": 8, "min": 0, "max": 512, "step": 1}),
"bg_color_hex": ("STRING", {"default": "#000000"}),
"bg_alpha": ("FLOAT", {"default": 0.50, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
# all shadows
"shadow_enable": ("BOOLEAN", {"default": False}),
"shadow_distance": ("INT", {"default": 4, "min": -50, "max": 50, "step": 1}),
"shadow_distance": ("INT", {"default": 3, "min": -50, "max": 50, "step": 1}),
"shadow_color_hex": ("STRING", {"default": "#000000"}),
"shadow_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
}
@@ -97,7 +99,7 @@ class TextOverlay:
print(f"Error loading font: {e} — using default font")
return ImageFont.load_default()
def _wrap_lines(self, draw, text, font, max_width, padding):
def _wrap_lines(self, draw, text, font, max_width, padding, letter_spacing):
"""
Greedy wrapping that respects explicit newlines and preserves spaces
(including multiple consecutive spaces).
@@ -108,75 +110,107 @@ class TextOverlay:
out_lines = []
for para in paragraphs:
# Keep empty lines exactly
if para == "":
out_lines.append("")
continue
# Tokens are either non-space runs or whitespace runs
tokens = re.findall(r"\S+|\s+", para)
line = ""
for tok in tokens:
candidate = line + tok
if draw.textlength(candidate, font=font) <= (max_width - 2 * padding):
# include tracking in the width test
char_count = max(0, len(candidate) - 1)
candidate_px = draw.textlength(candidate, font=font) + char_count * letter_spacing
if candidate_px <= (max_width - 2 * padding):
line = candidate
else:
if line == "":
# Single token longer than the max width: force-break
out_lines.append(candidate)
line = ""
else:
out_lines.append(line)
# Start new line with current token (trim leading spaces only)
line = tok.lstrip()
# Push whatever remains (don’t strip—preserve trailing spaces)
out_lines.append(line)
return out_lines
# ---------------- core drawing ----------------
def _compute_layout(self, img_w, img_h, draw, text, font, stroke_width, padding,
h_align, v_align, x_shift, y_shift, line_spacing, use_cache):
h_align, v_align, x_shift, y_shift, line_spacing, letter_spacing, font_size, use_cache):
"""
Manual multiline layout so stroke/fill line positions are identical.
IMPORTANT: We compute the visual extents (min_left / max_right) from each line's
textbbox (which already includes stroke when stroke_width > 0). This gives a
correct block width for the background box and eliminates the right-side gap
when stroke is enabled.
IMPORTANT: We compute the visual extents using per-line bbox plus letter spacing,
so background boxes and centering reflect tracking.
Returns:
(lines, widths, heights, tops, min_top, min_left, block_w, block_h, x0, visual_top_y)
"""
if self._cached is None or not use_cache:
lines = self._wrap_lines(draw, text, font, img_w, padding)
# derive a stable identifier for the font face (works across new FreeTypeFont instances)
try:
current_font_id = (font.getname(), getattr(font, "path", None))
except Exception:
current_font_id = (None, None)
need_recompute = True
if self._cached is not None and use_cache:
(*_, cached_letter_spacing, cached_stroke_width,
cached_text, cached_font_id, cached_font_size, cached_padding, cached_line_spacing) = self._cached
if (
cached_letter_spacing == letter_spacing and
cached_stroke_width == stroke_width and
cached_text == text and
cached_font_id == current_font_id and
cached_font_size == font_size and
cached_padding == padding and
cached_line_spacing == line_spacing
):
need_recompute = False
if need_recompute:
# wrap with spacing-aware width checks
lines = self._wrap_lines(draw, text, font, img_w, padding, letter_spacing)
widths, heights, tops = [], [], []
lefts, rights = [], []
lefts, rights_sp = [], []
for ln in lines:
# bbox includes stroke when stroke_width > 0
l, t, r, b = draw.textbbox((0, 0), ln, font=font, stroke_width=stroke_width)
widths.append(r - l)
heights.append(b - t)
w = (r - l)
h = (b - t)
extra = max(0, len(ln) - 1) * letter_spacing
widths.append(w + extra)
heights.append(h)
tops.append(t)
lefts.append(l)
rights.append(r)
rights_sp.append(r + extra)
min_left = min(lefts) if lefts else 0
max_right = max(rights) if rights else 0
block_w = max_right - min_left if rights else 0
max_right = max(rights_sp) if rights_sp else 0
block_w = max_right - min_left if rights_sp else 0
block_h = sum(heights) + (len(heights) - 1) * line_spacing if heights else 0
min_top = min(tops) if tops else 0 # may be negative for ascenders
min_top = min(tops) if tops else 0
# cache results to reuse on subsequent frames
self._cached = (lines, widths, heights, tops, min_top, min_left, block_w, block_h)
# cache results to reuse on subsequent frames (append both letter_spacing and stroke_width)
self._cached = (
lines, widths, heights, tops, min_top, min_left,
block_w, block_h,
letter_spacing, stroke_width,
text, current_font_id, font_size, padding, line_spacing
)
lines, widths, heights, tops, min_top, min_left, block_w, block_h = self._cached
# Horizontal anchor for the *visual* block (uses min_left/max_right)
# unpack cached values
(lines, widths, heights, tops, min_top, min_left,
block_w, block_h, cached_letter_spacing, cached_stroke_width, *_) = self._cached
# Horizontal anchor for the *visual* block (uses min_left/max_right w/ spacing)
if h_align == "left":
x0 = padding
elif h_align == "center":
@@ -204,6 +238,7 @@ class TextOverlay:
text,
all_caps,
font_size,
letter_spacing,
font,
fill_color_hex,
fill_alpha,
@@ -226,6 +261,7 @@ class TextOverlay:
shadow_color_hex,
shadow_alpha,
shadow_distance,
font_alignment,
use_cache=False,
):
# Prepare
@@ -249,15 +285,25 @@ class TextOverlay:
x0, visual_top_y) = self._compute_layout(
image.width, image.height, draw, text, self._loaded_font, sw,
padding, horizontal_alignment, vertical_alignment, x_shift, y_shift,
line_spacing, use_cache
line_spacing, letter_spacing, font_size, use_cache
)
# Convert the visual top into the baseline y for the first line
first_line_baseline_y = visual_top_y - min_top
# This is the x where we actually draw glyphs so that the visual left aligns to x0
x_draw = x0 - min_left
# Per-line offsets to align each line within the block
def _line_offset(i):
if font_alignment == "left":
return 0
elif font_alignment == "center":
return int(round((block_w - widths[i]) / 2))
else: # "right"
return int(round(block_w - widths[i]))
# Background box (perfectly aligned with visual extents)
if bg_enable and block_w > 0 and block_h > 0:
br, bgc, bb = self.hex_to_rgb(bg_color_hex)
@@ -281,12 +327,16 @@ class TextOverlay:
od = ImageDraw.Draw(overlay, "RGBA")
yy = first_line_baseline_y
for ln, h in zip(lines, heights):
od.text((x_draw + sdx, yy + sdy), ln, font=self._loaded_font, fill=(sh_r, sh_g, sh_b, sh_a))
for i, (ln, h) in enumerate(zip(lines, heights)):
xx = x_draw + _line_offset(i) # NEW
for ch in ln:
od.text((xx + sdx, yy + sdy), ch, font=self._loaded_font,
fill=(sh_r, sh_g, sh_b, sh_a))
xx += draw.textlength(ch, font=self._loaded_font) + letter_spacing
yy += int(round(h + line_spacing))
image = Image.alpha_composite(image, overlay)
# Stroke + Fill (per-line; identical geometry/positions)
fr, fg, fb = self.hex_to_rgb(fill_color_hex)
fa = int(max(0.0, min(1.0, fill_alpha)) * 255)
@@ -297,26 +347,20 @@ class TextOverlay:
od = ImageDraw.Draw(overlay, "RGBA")
yy = first_line_baseline_y
for ln, h in zip(lines, heights):
if sw > 0 and sa > 0:
od.text(
(x_draw, yy),
ln,
font=self._loaded_font,
fill=(sr, sg, sb, sa),
stroke_width=sw,
stroke_fill=(sr, sg, sb, sa),
)
if fa > 0:
od.text(
(x_draw, yy),
ln,
font=self._loaded_font,
fill=(fr, fg, fb, fa),
)
for i, (ln, h) in enumerate(zip(lines, heights)):
xx = x_draw + _line_offset(i) # NEW
for ch in ln:
if sw > 0 and sa > 0:
od.text((xx, yy), ch, font=self._loaded_font,
fill=(sr, sg, sb, sa),
stroke_width=sw, stroke_fill=(sr, sg, sb, sa))
if fa > 0:
od.text((xx, yy), ch, font=self._loaded_font, fill=(fr, fg, fb, fa))
xx += draw.textlength(ch, font=self._loaded_font) + letter_spacing
yy += int(round(h + line_spacing))
image = Image.alpha_composite(image, overlay)
return image.convert("RGB")
# ---------------- Comfy entrypoint ----------------
@@ -325,8 +369,9 @@ class TextOverlay:
self,
image,
text,
all_caps, # NEW
all_caps,
font_size,
letter_spacing,
font,
fill_color_hex,
fill_alpha,
@@ -349,6 +394,7 @@ class TextOverlay:
shadow_color_hex,
shadow_alpha,
shadow_distance,
font_alignment,
):
"""Handles both single and batch image processing (with layout caching across frames)."""
@@ -358,14 +404,14 @@ class TextOverlay:
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
out_img = self.draw_text(
pil_img, text, all_caps,
font_size, font,
font_size, letter_spacing, font,
fill_color_hex, fill_alpha,
stroke_enable,
stroke_color_hex, stroke_alpha, stroke_thickness,
padding, horizontal_alignment, vertical_alignment,
x_shift, y_shift, line_spacing,
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance,
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance,font_alignment,
use_cache=False,
)
out = np.array(out_img).astype(np.float32) / 255.0
@@ -380,14 +426,14 @@ class TextOverlay:
pil_img = Image.fromarray((arr * 255).astype(np.uint8))
out_img = self.draw_text(
pil_img, text, all_caps,
font_size, font,
font_size, letter_spacing, font,
fill_color_hex, fill_alpha,
stroke_enable,
stroke_color_hex, stroke_alpha, stroke_thickness,
padding, horizontal_alignment, vertical_alignment,
x_shift, y_shift, line_spacing,
bg_enable, bg_color_hex, bg_alpha, bg_padding, bg_radius,
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance,
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance,font_alignment,
use_cache=use_cache, # reuse wrapped lines/metrics for next frames
)
outs.append(np.array(out_img).astype(np.float32) / 255.0)
+13
View File
@@ -0,0 +1,13 @@
# pyproject.toml
[project]
name = "advanced-textoverlay" # Unique and immutable node identifier.
description = "Advanced Text Overlay Node for ComfyUI featuring rebuilt rendering logic, precise alignment, custom fonts, and extended styling controls."
version = "1.2.0" # Semantic versioning
dependencies = [] # Populated from requirements.txt if present.
[project.urls]
Repository = "https://github.com/scofano/ComfyUI-Advanced-TextOverlay"
[tool.comfy]
PublisherId = "dansco"
DisplayName = "Advanced Text Overlay"
+3
View File
@@ -0,0 +1,3 @@
numpy
torch
Pillow