Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb7ce1d85 | ||
|
|
673e9833eb | ||
|
|
923879040a | ||
|
|
bc5428af5d | ||
|
|
ba0a48677e | ||
|
|
830be708b6 | ||
|
|
7112fd3f09 | ||
|
|
5a49ca6ef1 | ||
|
|
40d045ec35 | ||
|
|
9c74a78c98 | ||
|
|
dedd7b88f5 | ||
|
|
62617e530a |
@@ -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.
|
||||
|
||||
|
||||
---
|
||||

|
||||
|
||||
## ✨ 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)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
@@ -38,15 +38,16 @@ 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}),
|
||||
"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,7 +58,7 @@ 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"}),
|
||||
@@ -97,7 +98,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 +109,83 @@ 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, 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)
|
||||
need_recompute = True
|
||||
if self._cached is not None and use_cache:
|
||||
# the last element of the cache is the letter_spacing it was computed with
|
||||
cached_letter_spacing = self._cached[-1]
|
||||
if cached_letter_spacing == letter_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)
|
||||
# add extra visual width from inter-char spacing
|
||||
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
|
||||
|
||||
# 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 letter_spacing)
|
||||
self._cached = (lines, widths, heights, tops, min_top, min_left, block_w, block_h, letter_spacing)
|
||||
|
||||
lines, widths, heights, tops, min_top, min_left, block_w, block_h = self._cached
|
||||
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)
|
||||
# Horizontal anchor for the *visual* block (uses min_left/max_right w/ spacing)
|
||||
if h_align == "left":
|
||||
x0 = padding
|
||||
elif h_align == "center":
|
||||
@@ -198,12 +207,14 @@ class TextOverlay:
|
||||
|
||||
return lines, widths, heights, tops, min_top, min_left, block_w, block_h, x0, visual_top_y
|
||||
|
||||
|
||||
def draw_text(
|
||||
self,
|
||||
image,
|
||||
text,
|
||||
all_caps,
|
||||
font_size,
|
||||
letter_spacing,
|
||||
font,
|
||||
fill_color_hex,
|
||||
fill_alpha,
|
||||
@@ -249,9 +260,9 @@ 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, use_cache
|
||||
)
|
||||
|
||||
|
||||
# Convert the visual top into the baseline y for the first line
|
||||
first_line_baseline_y = visual_top_y - min_top
|
||||
|
||||
@@ -282,11 +293,15 @@ class TextOverlay:
|
||||
|
||||
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))
|
||||
xx = x_draw
|
||||
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)
|
||||
@@ -298,25 +313,33 @@ class TextOverlay:
|
||||
|
||||
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),
|
||||
)
|
||||
# draw one character at a time to inject letter spacing
|
||||
xx = x_draw
|
||||
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),
|
||||
)
|
||||
|
||||
# advance by glyph width + custom letter spacing
|
||||
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 +348,9 @@ class TextOverlay:
|
||||
self,
|
||||
image,
|
||||
text,
|
||||
all_caps, # NEW
|
||||
all_caps,
|
||||
font_size,
|
||||
letter_spacing,
|
||||
font,
|
||||
fill_color_hex,
|
||||
fill_alpha,
|
||||
@@ -358,7 +382,7 @@ 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,
|
||||
@@ -380,7 +404,7 @@ 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,
|
||||
|
||||
@@ -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.1.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"
|
||||
@@ -0,0 +1,3 @@
|
||||
numpy
|
||||
torch
|
||||
Pillow
|
||||
Reference in New Issue
Block a user