8 Commits
Author SHA1 Message Date
scofano 89992997c5 Rename animated2.gif to animated.gif 2025-10-29 11:02:01 -03:00
scofano 67d3c6d9d4 Rename animated.gif to animated2.gif 2025-10-29 11:01:44 -03:00
scofano 29e0bbdad6 Bump version to 1.3.0
Update the project version in pyproject.toml from 1.2.0 to 1.3.0 to reflect new changes or features.
2025-10-29 10:59:56 -03:00
scofano dbf6449949 Add animation feature to text overlay node
Introduces a new animation capability for the text overlay node, supporting various animation types (fade, move) and easing modes. The README is updated with detailed documentation on animation parameters, supported types, and technical integration.
2025-10-29 10:58:18 -03:00
scofano 405e4b4d19 Add animation support to TextOverlay node
Introduces a new animations.py module with easing, opacity, and offset helpers. Updates nodes.py to support animated text overlays, including fade and move-in effects, with new parameters for animation kind, duration, easing, and opacity. The node now handles both single images and batches, animating the overlay for a configurable number of frames and holding the final pose for the remainder of the sequence.
2025-10-29 10:54:00 -03:00
scofano 22b183168d Change padding and shift properties to Integer 2025-10-29 08:43:52 -03:00
scofano 6f690c48f7 Merge branch 'main' of https://github.com/scofano/ComfyUI-Advanced-TextOverlay 2025-10-28 12:05:55 -03:00
scofano 824b888222 Parameters order changed 2025-10-28 12:05:51 -03:00
4 changed files with 297 additions and 119 deletions
+55 -5
View File
@@ -41,13 +41,63 @@ This node extends and refines the classic ComfyUI Text Overlay concept with a **
* Supports both `.ttf` and `.otf` formats.
* Graceful fallback to a system font if a specified font is unavailable.
### ⚙️ 5. Batch and Workflow Enhancements
### 🎞️ 5. New Animation Feature
* The node now supports smooth text animations via the new animations.py module.
* Each animation type manipulates opacity and/or position offsets frame-by-frame.
Supported Animation Types:
| Animation Kind | Description |
| -------------------- | --------------------------------------------------------- |
| **fade_in** | Gradually increases opacity from 0 → target. |
| **fade_out** | Gradually decreases opacity from target → 0. |
| **move_from_top** | Moves text upward from off-screen top into position. |
| **move_from_bottom** | Moves text downward from off-screen bottom into position. |
| **move_from_left** | Moves text from left edge into position. |
| **move_from_right** | Moves text from right edge into position. |
Easing Modes:
| Easing | Behavior |
| --------------- | ------------------------------------- |
| **linear** | Uniform speed. |
| **ease_in** | Starts slow, speeds up. |
| **ease_out** | Starts fast, slows down. |
| **ease_in_out** | Smooth acceleration and deceleration. |
Animation Parameters
| Parameter | Type | Description |
| ---------------------------- | ------- | ----------------------------------------------------------- |
| **animate** | Boolean | Enable or disable animation. |
| **animation_kind** | Enum | Selects animation type (`fade_in`, `move_from_left`, etc.). |
| **animation_frames** | Integer | Number of frames to complete the animation. |
| **animation_ease** | Enum | Easing curve for smooth motion. |
| **animation_opacity_target** | Float | Target opacity multiplier for animation. |
Behavior:
* For single images, the node outputs either one static frame or an animated sequence.
* For batches/videos, animation applies over the first animation_frames frames, then holds the final pose.
Technical Summary:
The animation module provides:
* progress(t, T_minus1, ease): easing interpolation over frames.
* compute_opacity(kind, p, target): calculates alpha fade transitions.
* compute_offsets(kind, p, img_w, img_h): determines X/Y displacement for move animations.
These functions are integrated directly into the node’s rendering loop for seamless animated overlays
### ⚙️ 6. Batch and Workflow Enhancements
* Native **batch support** — automatically applies consistent overlay logic across all input images.
* Optimized performance and memory use when processing large image sets.
* Node parameters persist and preview correctly in ComfyUI sessions.
### 🧩 6. Developer & Extensibility Features
### 🧩 7. Developer & Extensibility Features
* Modularized code for easier maintenance and feature additions.
* Clean separation between text layout and rendering logic.
@@ -100,11 +150,11 @@ To install the **Advanced Text Overlay** node:
| **fill_color_hex** | String | Text fill color in hex format (e.g., `#FFFFFF`). |
| **stroke_color_hex** | String | Stroke (outline) color in hex format (e.g., `#000000`). |
| **stroke_thickness** | Float | Thickness of the text outline. |
| **padding** | Float | Additional space around the text block. Useful for multi-line layouts. |
| **padding** | Integer | Additional space around the text block. Useful for multi-line layouts. |
| **horizontal_alignment** | Enum | `left`, `center`, or `right` horizontal alignment. |
| **vertical_alignment** | Enum | `top`, `middle`, or `bottom` vertical alignment. |
| **x_shift** | Float | Horizontal offset adjustment (pixels). |
| **y_shift** | Float | Vertical offset adjustment (pixels). |
| **x_shift** | Integer | Horizontal offset adjustment (pixels). |
| **y_shift** | Integer | Vertical offset adjustment (pixels). |
| **line_spacing** | Float | Vertical spacing between lines of text. |
| **letter_spacing** | Float | Horizontal spacing between letters. |
+48
View File
@@ -0,0 +1,48 @@
from typing import Tuple
# Easing -------------------------------------------------------------
def progress(t: int, T_minus1: int, ease: str) -> float:
if T_minus1 <= 0:
return 1.0
x = float(t) / float(T_minus1)
if ease == "linear":
return x
if ease == "ease_in":
return x * x
if ease == "ease_out":
return 1 - (1 - x) * (1 - x)
if ease == "ease_in_out":
if x < 0.5:
return 2 * x * x
return 1 - (-2 * x + 2) ** 2 / 2
return x
# Opacity ------------------------------------------------------------
def compute_opacity(kind: str, p: float, target: float) -> float:
"""
kind: 'fade_in' | 'fade_out' | others
p in [0,1], target in [0,1]
"""
target = max(0.0, min(1.0, float(target)))
if kind == "fade_out":
return target * (1.0 - p)
if kind == "fade_in":
return target * p
return target
# Offsets ------------------------------------------------------------
def compute_offsets(kind: str, p: float, img_w: int, img_h: int) -> Tuple[int, int]:
"""
Move from off-screen-ish towards the final position.
p=0 => max offset, p=1 => 0 offset.
"""
dx = dy = 0.0
if kind == "move_from_left":
dx = -(img_w * (1.0 - p) * 0.5)
elif kind == "move_from_right":
dx = (img_w * (1.0 - p) * 0.5)
elif kind == "move_from_top":
dy = -(img_h * (1.0 - p) * 0.5)
elif kind == "move_from_bottom":
dy = (img_h * (1.0 - p) * 0.5)
return int(round(dx)), int(round(dy))
+193 -113
View File
@@ -1,34 +1,31 @@
import os
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont, ImageChops
# Relative import so it works as a package module in ComfyUI
from . import animations
class TextOverlay:
"""
Text overlay node with:
- Fill/stroke alpha
- Shadow and background box
- Shadow and background box (both animate with opacity)
- Pixel-perfect stroke alignment for MULTILINE text
- Even stroke width for crisper edges
- Default vertical_alignment = 'middle'
- Batch animation: uses the first `animation_frames` frames for the animation,
then holds the final pose for the rest of the video.
- Mask animations removed. Renamed kinds: fade_in/fade_out, move_from_*.
"""
_horizontal_alignments = ["left", "center", "right"]
_vertical_alignments = ["top", "middle", "bottom"]
def __init__(self, device="cpu"):
self.device = device
self._loaded_font = None
# cache (lines, widths, heights, tops, min_top, min_left, block_w, block_h)
self._cached = None
self._x = None
self._y = None
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
# keep image first so the node wiring stays obvious
"image": ("IMAGE",),
# UI order (top → bottom)
@@ -43,7 +40,7 @@ class TextOverlay:
"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, alignment, offsets
"padding": ("INT", {"default": 16, "min": 0, "max": 1024, "step": 1}),
"vertical_alignment": (cls._vertical_alignments, {"default": "middle"}),
"y_shift": ("INT", {"default": 0, "min": -1024, "max": 1024, "step": 1}),
@@ -51,24 +48,34 @@ class TextOverlay:
"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
# strokes
"stroke_enable": ("BOOLEAN", {"default": True}),
"stroke_color_hex": ("STRING", {"default": "#000000"}),
"stroke_thickness": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
"stroke_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
# all boxes
# background box
"bg_enable": ("BOOLEAN", {"default": False}),
"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
"shadow_enable": ("BOOLEAN", {"default": False}),
"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"}),
"shadow_alpha": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
# --- animation controls ---
"animate": ("BOOLEAN", {"default": False}),
"animation_kind": ([
"fade_in", "fade_out",
"move_from_top", "move_from_bottom", "move_from_left", "move_from_right",
], {"default": "fade_in"}),
"animation_frames": ("INT", {"default": 32, "min": 1, "max": 1000, "step": 1}),
"animation_ease": (["linear","ease_in","ease_out","ease_in_out"], {"default": "ease_in_out"}),
"animation_opacity_target": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "slider"}),
}
}
@@ -85,14 +92,13 @@ class TextOverlay:
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
def _normalize_text(self, text: str) -> str:
# Convert literal "\n" to actual newlines
return text.replace("\\n", "\n").replace("\\N", "\n")
def _load_font(self, font, font_size):
fonts_dir = os.path.join(os.path.dirname(__file__), "fonts")
font_path = os.path.join(fonts_dir, font)
if not os.path.exists(font_path):
font_path = font # fallback to system path or provided absolute path
font_path = font
try:
return ImageFont.truetype(font_path, font_size)
except Exception as e:
@@ -100,26 +106,17 @@ class TextOverlay:
return ImageFont.load_default()
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).
"""
import re
paragraphs = self._normalize_text(text).split("\n")
out_lines = []
for para in paragraphs:
if para == "":
out_lines.append("")
continue
tokens = re.findall(r"\S+|\s+", para)
line = ""
for tok in tokens:
candidate = line + tok
# 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):
@@ -131,52 +128,33 @@ class TextOverlay:
else:
out_lines.append(line)
line = tok.lstrip()
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, letter_spacing, font_size, use_cache):
"""
Manual multiline layout so stroke/fill line positions are identical.
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)
"""
# derive a stable identifier for the font face (works across new FreeTypeFont instances)
h_align, v_align, x_shift, y_shift, line_spacing, letter_spacing, font_size, use_cache):
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:
if hasattr(self, "_cached") and 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
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
):
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_sp = [], []
for ln in lines:
@@ -196,7 +174,6 @@ class TextOverlay:
block_h = sum(heights) + (len(heights) - 1) * line_spacing if heights else 0
min_top = min(tops) if tops else 0
# 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,
@@ -204,29 +181,23 @@ class TextOverlay:
text, current_font_id, font_size, padding, line_spacing
)
# unpack cached values
(lines, widths, heights, tops, min_top, min_left,
block_w, block_h, cached_letter_spacing, cached_stroke_width, *_) = self._cached
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":
x0 = (img_w - block_w) / 2
else: # right
else:
x0 = img_w - block_w - padding
# Vertical anchor for the *visual* block
if v_align == "top":
visual_top_y = padding
elif v_align == "middle":
visual_top_y = (img_h - block_h) / 2
else: # bottom
else:
visual_top_y = img_h - block_h - padding
# Snap + shifts
x0 = int(round(x0 + x_shift))
visual_top_y = int(round(visual_top_y + y_shift))
@@ -263,51 +234,50 @@ class TextOverlay:
shadow_distance,
font_alignment,
use_cache=False,
opacity_scale=1.0,
dx=0,
dy=0
):
# Prepare
if image.mode != "RGBA":
image = image.convert("RGBA")
self._loaded_font = self._load_font(font, font_size)
loaded_font = self._load_font(font, font_size)
draw = ImageDraw.Draw(image, "RGBA")
# Apply ALL CAPS if requested (after literal "\n" handling via _wrap_lines)
if all_caps:
# Uppercase before wrapping so measurement/layout reflect final glyphs
text = text.upper()
# Compute even stroke width (recommended for crisp outlines)
opacity_scale = max(0.0, min(1.0, float(opacity_scale)))
fill_alpha = max(0.0, min(1.0, float(fill_alpha) * opacity_scale))
stroke_alpha = max(0.0, min(1.0, float(stroke_alpha) * opacity_scale))
x_shift = int(round(x_shift + dx))
y_shift = int(round(y_shift + dy))
sw = int(round(font_size * stroke_thickness * 0.5)) if stroke_enable else 0
if sw % 2 == 1 and sw > 0:
sw += 1
# Layout (manual lines)
(lines, widths, heights, tops, min_top, min_left, block_w, block_h,
x0, visual_top_y) = self._compute_layout(
image.width, image.height, draw, text, self._loaded_font, sw,
image.width, image.height, draw, text, loaded_font, sw,
padding, horizontal_alignment, vertical_alignment, x_shift, y_shift,
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"
else:
return int(round(block_w - widths[i]))
# Background box (perfectly aligned with visual extents)
# Background (animated alpha)
if bg_enable and block_w > 0 and block_h > 0:
br, bgc, bb = self.hex_to_rgb(bg_color_hex)
ba = int(max(0.0, min(1.0, bg_alpha)) * 255)
ba = int(max(0.0, min(1.0, float(bg_alpha) * opacity_scale)) * 255)
rect = [x0 - bg_padding, visual_top_y - bg_padding,
x0 + block_w + bg_padding, visual_top_y + block_h + bg_padding]
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
@@ -318,26 +288,26 @@ class TextOverlay:
od.rectangle(rect, fill=(br, bgc, bb, ba))
image = Image.alpha_composite(image, overlay)
# Shadow (per-line, before stroke/fill)
# Shadow (animated alpha)
if shadow_enable and block_w > 0 and block_h > 0:
sh_r, sh_g, sh_b = self.hex_to_rgb(shadow_color_hex)
sh_a = int(max(0.0, min(1.0, shadow_alpha)) * 255)
sh_a = int(max(0.0, min(1.0, float(shadow_alpha) * opacity_scale)) * 255)
sdx = sdy = int(shadow_distance)
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
od = ImageDraw.Draw(overlay, "RGBA")
yy = first_line_baseline_y
for i, (ln, h) in enumerate(zip(lines, heights)):
xx = x_draw + _line_offset(i) # NEW
xx = x_draw + _line_offset(i)
for ch in ln:
od.text((xx + sdx, yy + sdy), ch, font=self._loaded_font,
od.text((xx + sdx, yy + sdy), ch, font=loaded_font,
fill=(sh_r, sh_g, sh_b, sh_a))
xx += draw.textlength(ch, font=self._loaded_font) + letter_spacing
xx += draw.textlength(ch, font=loaded_font) + letter_spacing
yy += int(round(h + line_spacing))
image = Image.alpha_composite(image, overlay)
# Stroke + Fill (per-line; identical geometry/positions)
# Stroke + Fill
fr, fg, fb = self.hex_to_rgb(fill_color_hex)
fa = int(max(0.0, min(1.0, fill_alpha)) * 255)
sr, sg, sb = self.hex_to_rgb(stroke_color_hex)
@@ -348,15 +318,15 @@ class TextOverlay:
yy = first_line_baseline_y
for i, (ln, h) in enumerate(zip(lines, heights)):
xx = x_draw + _line_offset(i) # NEW
xx = x_draw + _line_offset(i)
for ch in ln:
if sw > 0 and sa > 0:
od.text((xx, yy), ch, font=self._loaded_font,
od.text((xx, yy), ch, font=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
od.text((xx, yy), ch, font=loaded_font, fill=(fr, fg, fb, fa))
xx += draw.textlength(ch, font=loaded_font) + letter_spacing
yy += int(round(h + line_spacing))
image = Image.alpha_composite(image, overlay)
@@ -395,35 +365,29 @@ class TextOverlay:
shadow_alpha,
shadow_distance,
font_alignment,
animate=False,
animation_kind='fade_in',
animation_frames=24,
animation_ease='ease_out',
animation_opacity_target=1.0
):
"""Handles both single and batch image processing (with layout caching across frames)."""
"""
Single image (H,W,C):
- animate=False -> one output image
- animate=True -> returns exactly `animation_frames` frames
# Single image
Batch (B,H,W,C) video:
- animate=False -> draw per-frame with no animation changes
- animate=True -> animation starts at frame 0 and completes at frame `animation_frames-1`.
From then on, the final pose is held until frame B-1.
"""
# Single image (H, W, C)
if len(image.shape) == 3:
np_img = image.cpu().numpy()
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
out_img = self.draw_text(
pil_img, text, all_caps,
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,font_alignment,
use_cache=False,
)
out = np.array(out_img).astype(np.float32) / 255.0
return (torch.tensor(out),)
# Batch of images
else:
np_imgs = image.cpu().numpy()
outs = []
use_cache = False
for arr in np_imgs:
pil_img = Image.fromarray((arr * 255).astype(np.uint8))
if not animate:
out_img = self.draw_text(
pil_img, text, all_caps,
font_size, letter_spacing, font,
@@ -433,11 +397,127 @@ class TextOverlay:
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,font_alignment,
use_cache=use_cache, # reuse wrapped lines/metrics for next frames
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance, font_alignment,
use_cache=False,
)
out = np.array(out_img).astype(np.float32) / 255.0
return (torch.tensor(out),)
T = max(1, int(animation_frames))
outs = []
# Prime layout cache
_ = self.draw_text(
pil_img, text, all_caps,
font_size, letter_spacing, font,
fill_color_hex, 1.0,
stroke_enable,
stroke_color_hex, 1.0, 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, font_alignment,
use_cache=False,
)
use_cache = True
for t_idx in range(T):
p = animations.progress(t_idx, max(1, T - 1), animation_ease)
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
out_img = self.draw_text(
pil_img, text, all_caps,
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, font_alignment,
use_cache=use_cache,
opacity_scale=op,
dx=dx,
dy=dy,
)
outs.append(np.array(out_img).astype(np.float32) / 255.0)
use_cache = True
return (torch.tensor(np.stack(outs)),)
# Batch (B, H, W, C)
if not (hasattr(image, "shape") and len(image.shape) == 4):
raise ValueError("Unsupported image tensor shape")
B, H, W, C = image.shape
if not animate:
out_list = []
for i in range(B):
np_img = image[i].cpu().numpy()
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
out_img = self.draw_text(
pil_img, text, all_caps,
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, font_alignment,
use_cache=False,
)
out_list.append(np.array(out_img).astype(np.float32) / 255.0)
return (torch.tensor(np.stack(out_list)),)
# Animated batch: animate on frames [0 .. T-1], then hold on frames [T .. B-1]
T = max(1, int(animation_frames))
# Prime cache once using first frame
np_img0 = image[0].cpu().numpy()
pil_img0 = Image.fromarray((np_img0 * 255).astype(np.uint8))
_ = self.draw_text(
pil_img0, text, all_caps,
font_size, letter_spacing, font,
fill_color_hex, 1.0,
stroke_enable,
stroke_color_hex, 1.0, 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, font_alignment,
use_cache=False,
)
use_cache = True
out_list = []
for i in range(B):
np_img = image[i].cpu().numpy()
pil_img = Image.fromarray((np_img * 255).astype(np.uint8))
eff_t = min(i, T - 1) # frames beyond T-1 hold the last pose
p = animations.progress(eff_t, max(1, T - 1), animation_ease)
op = animations.compute_opacity(animation_kind, p, float(animation_opacity_target))
dx, dy = animations.compute_offsets(animation_kind, p, pil_img.width, pil_img.height)
out_img = self.draw_text(
pil_img, text, all_caps,
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, font_alignment,
use_cache=use_cache,
opacity_scale=op,
dx=dx,
dy=dy,
)
out_list.append(np.array(out_img).astype(np.float32) / 255.0)
return (torch.tensor(np.stack(out_list)),)
NODE_CLASS_MAPPINGS = {"Advanced Text Overlay": TextOverlay}
+1 -1
View File
@@ -2,7 +2,7 @@
[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
version = "1.3.0" # Semantic versioning
dependencies = [] # Populated from requirements.txt if present.
[project.urls]