Added Font Alignment

This commit is contained in:
scofano
2025-10-28 11:27:10 -03:00
parent bdb7ce1d85
commit bcfb465380
2 changed files with 62 additions and 40 deletions
+61 -39
View File
@@ -39,6 +39,7 @@ class TextOverlay:
"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"}),
@@ -65,7 +66,7 @@ class TextOverlay:
# 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"}),
}
@@ -139,7 +140,7 @@ class TextOverlay:
# ---------------- 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, 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.
@@ -148,13 +149,30 @@ class TextOverlay:
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)
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:
# 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:
(*_, 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)
@@ -162,11 +180,9 @@ class TextOverlay:
widths, heights, tops = [], [], []
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)
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)
@@ -178,12 +194,21 @@ class TextOverlay:
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 (append letter_spacing)
self._cached = (lines, widths, heights, tops, min_top, min_left, block_w, block_h, letter_spacing)
# 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
)
# unpack cached values
(lines, widths, heights, tops, min_top, min_left,
block_w, block_h, cached_letter_spacing, cached_stroke_width, *_) = 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 w/ spacing)
if h_align == "left":
@@ -207,7 +232,6 @@ class TextOverlay:
return lines, widths, heights, tops, min_top, min_left, block_w, block_h, x0, visual_top_y
def draw_text(
self,
image,
@@ -237,6 +261,7 @@ class TextOverlay:
shadow_color_hex,
shadow_alpha,
shadow_distance,
font_alignment,
use_cache=False,
):
# Prepare
@@ -260,8 +285,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, letter_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
@@ -269,6 +295,15 @@ class TextOverlay:
# 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)
@@ -292,13 +327,13 @@ class TextOverlay:
od = ImageDraw.Draw(overlay, "RGBA")
yy = first_line_baseline_y
for ln, h in zip(lines, heights):
xx = x_draw
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))
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)
@@ -312,30 +347,16 @@ class TextOverlay:
od = ImageDraw.Draw(overlay, "RGBA")
yy = first_line_baseline_y
for ln, h in zip(lines, heights):
# draw one character at a time to inject letter spacing
xx = x_draw
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),
)
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
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)
@@ -373,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)."""
@@ -389,7 +411,7 @@ 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,
shadow_enable, shadow_color_hex, shadow_alpha, shadow_distance,font_alignment,
use_cache=False,
)
out = np.array(out_img).astype(np.float32) / 255.0
@@ -411,7 +433,7 @@ 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,
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)
+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.1.0" # Semantic versioning
version = "1.2.0" # Semantic versioning
dependencies = [] # Populated from requirements.txt if present.
[project.urls]