Files
Koorosh Ghotb 54d5f1f7d4 Update PersianText.py
Added mixed text input, transparent background, HEX code reading, minor updates.
2025-05-13 13:08:09 +03:30

283 lines
12 KiB
Python

import os
import torch
from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter
import arabic_reshaper
import numpy as np
import unicodedata
from bidi.algorithm import get_display
from .utils.colors import COLORS, color_mapping
import re
class PersianText:
@classmethod
def INPUT_TYPES(cls):
font_dir = os.path.join(os.path.dirname(__file__), "Fonts")
font_files = []
try:
font_files = [f for f in os.listdir(font_dir) if f.endswith(('.ttf', '.otf'))]
except FileNotFoundError:
font_files = []
return {
"required": {
"Text": ("STRING", {"default": "سلام کامفی", "dynamicPrompts": True, "rows": 5, "multiline": True}),
"Farsi/Arabic Font": (["default"] + font_files, {"default": "BYekan.ttf"}),
"English/Latin Font": (["default"] + font_files, {"default": "arial.ttf"}),
"Size": ("INT", {"default": 60, "min": 1, "max": 256}),
"Text Color": (COLORS, {"default": "Black"}),
"Background Color": (COLORS, {"default": "White"}),
"Horizontal Align": (["left", "center", "right"], {"default": "center"}),
"Vertical Align": (["top", "center", "bottom"], {"default": "center"}),
"Image Width": ("INT", {"default": 512, "min": 1, "max": 4096}),
"Image Height": ("INT", {"default": 512, "min": 1, "max": 4096}),
"Rotation": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0}),
"Offset X": ("INT", {"default": 0, "min": -256, "max": 256}),
"Offset Y": ("INT", {"default": 0, "min": -256, "max": 256}),
"Shadow Distance": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 256.0}),
"Shadow Blur": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 256.0}),
"Shadow Color": (COLORS, {"default": "Black"}),
"Text Color Hex": ("STRING", {"default": "#000000"}),
"Background Color Hex": ("STRING", {"default": "#FFFFFF"}),
"Shadow Color Hex": ("STRING", {"default": "#000000"}),
"Padding": ("INT", {"default": 0, "min": 0, "max": 256}),
"Transparent Background": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "execute"
CATEGORY = "🎨KG"
def process_text(self, text, direction="auto"):
def get_script(char):
category = unicodedata.category(char)
if char == ' ':
return "space"
elif unicodedata.bidirectional(char) in ('AL', 'R', 'AN'):
return "arabic"
else:
return "latin"
lines = text.split('\n')
processed_lines = []
for line in lines:
if not line.strip():
processed_lines.append([])
continue
segments = []
current_segment = ""
current_script = None
for char in line:
script = get_script(char)
if script == "space" and current_segment:
current_segment += char
elif script != current_script and current_segment:
segments.append((current_script, current_segment))
current_segment = char
current_script = script
else:
current_segment += char
current_script = script
if current_segment:
segments.append((current_script, current_segment))
processed_segments = []
for script, segment in segments:
if script == "arabic":
reshaped = arabic_reshaper.reshape(segment)
processed_segment = get_display(reshaped)
else:
processed_segment = segment
processed_segments.append((script, processed_segment))
processed_lines.append(processed_segments)
return processed_lines
def execute(self, **kwargs):
text = kwargs.get("Text", "سلام کامفی")
farsi_font = kwargs.get("Farsi/Arabic Font", "BYekan.ttf")
latin_font = kwargs.get("English/Latin Font", "arial.ttf")
size = kwargs.get("Size", 60)
text_color = kwargs.get("Text Color", "Black")
background_color = kwargs.get("Background Color", "White")
horizontal_align = kwargs.get("Horizontal Align", "center")
vertical_align = kwargs.get("Vertical Align", "center")
image_width = kwargs.get("Image Width", 512)
image_height = kwargs.get("Image Height", 512)
rotation = kwargs.get("Rotation", 0.0)
offset_x = kwargs.get("Offset X", 0)
offset_y = kwargs.get("Offset Y", 0)
shadow_distance = kwargs.get("Shadow Distance", 0)
shadow_blur = kwargs.get("Shadow Blur", 0)
shadow_color = kwargs.get("Shadow Color", "Black")
text_color_hex = kwargs.get("Text Color Hex", "#000000")
background_color_hex = kwargs.get("Background Color Hex", "#FFFFFF")
shadow_color_hex = kwargs.get("Shadow Color Hex", "#000000")
padding = kwargs.get("Padding", 0)
transparent_background = kwargs.get("Transparent Background", False)
# Validate and process HEX colors
def validate_hex_color(hex_str, default_rgb):
if not hex_str or not isinstance(hex_str, str):
return default_rgb
hex_str = hex_str.strip()
# Remove leading '#' if present
hex_str = hex_str.lstrip('#')
# Check if the string is a valid HEX color (6 or 8 characters)
if re.match(r'^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$', hex_str):
try:
return ImageColor.getrgb(f"#{hex_str}")
except ValueError:
return default_rgb
return default_rgb
# Assign colors with validation
text_rgb = color_mapping.get(text_color, (0, 0, 0)) if text_color != "Custom (HEX Color)" else validate_hex_color(text_color_hex, (0, 0, 0))
background_rgb = color_mapping.get(background_color, (255, 255, 255)) if background_color != "Custom (HEX Color)" else validate_hex_color(background_color_hex, (255, 255, 255))
shadow_rgb = color_mapping.get(shadow_color, (0, 0, 0)) if shadow_color != "Custom (HEX Color)" else validate_hex_color(shadow_color_hex, (0, 0, 0))
font_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "Fonts")
farsi_font_path = os.path.join(font_dir, "arial.ttf" if farsi_font == "default" else farsi_font)
latin_font_path = os.path.join(font_dir, "arial.ttf" if latin_font == "default" else latin_font)
try:
arabic_selected_font = ImageFont.truetype(farsi_font_path, size)
latin_selected_font = ImageFont.truetype(latin_font_path, size)
except Exception as e:
raise ValueError(f"Failed to load fonts: {str(e)}")
processed_lines = self.process_text(text, "auto")
# Initialize background and mask layer
background = Image.new("RGBA", (image_width, image_height), background_rgb + (255,) if not transparent_background else (0, 0, 0, 0))
mask_layer = Image.new("L", (image_width, image_height), 0)
mask_draw = ImageDraw.Draw(mask_layer)
# Calculate text dimensions and positions
padded_width = image_width - 2 * padding
padded_height = image_height - 2 * padding
line_heights = []
segment_widths = []
segment_fonts = []
for line in processed_lines:
max_height = 0
line_width = 0
line_segment_widths = []
line_segment_fonts = []
for j, (script, segment) in enumerate(line):
selected_font = arabic_selected_font if script == "arabic" else latin_selected_font
bbox = selected_font.getbbox(segment)
segment_width = bbox[2] - bbox[0]
line_segment_widths.append(segment_width)
line_segment_fonts.append(selected_font)
line_width += segment_width
max_height = max(max_height, bbox[3] - bbox[1])
if j < len(line) - 1:
line_width += 10
line_heights.append(max_height)
segment_widths.append(line_segment_widths)
segment_fonts.append(line_segment_fonts)
total_text_height = sum(line_heights) + 10 * (len(processed_lines) - 1)
# Vertical alignment
if vertical_align == "top":
y_start = padding + offset_y
elif vertical_align == "bottom":
y_start = image_height - padding - total_text_height + offset_y
else:
y_start = (image_height - total_text_height) // 2 + offset_y
# Draw text on mask_layer
y = y_start
for i, line in enumerate(processed_lines):
if not line:
y += line_heights[i] + 10
continue
line_width = sum(segment_widths[i]) + 10 * (len(line) - 1)
rtl_chars = sum(len(seg) for script, seg in line if script == "arabic")
total_chars = sum(len(seg) for script, seg in line)
is_rtl_line = rtl_chars > total_chars / 2
if horizontal_align == "left":
x_start = padding + offset_x
elif horizontal_align == "center":
x_start = (padded_width - line_width) // 2 + offset_x
else:
x_start = padded_width - line_width - padding + offset_x
if is_rtl_line:
current_x = x_start + line_width
else:
current_x = x_start
line_segments = reversed(line) if is_rtl_line else line
segment_widths_for_line = reversed(segment_widths[i]) if is_rtl_line else segment_widths[i]
segment_fonts_for_line = reversed(segment_fonts[i]) if is_rtl_line else segment_fonts[i]
for (script, segment), segment_width, selected_font in zip(line_segments, segment_widths_for_line, segment_fonts_for_line):
spacing = 10 if len(line) > 1 else 0
if is_rtl_line:
draw_x = current_x - segment_width
else:
draw_x = current_x
mask_draw.text(
(draw_x, y),
segment,
font=selected_font,
fill=255
)
if is_rtl_line:
current_x -= (segment_width + spacing)
else:
current_x += (segment_width + spacing)
y += line_heights[i] + 10
# Apply shadow if enabled
if shadow_distance > 0 or shadow_blur > 0:
shadow_mask = Image.new("L", background.size, 0)
offset_x_shadow = int(shadow_distance)
offset_y_shadow = int(shadow_distance)
# Paste mask_layer at offset for shadow
shadow_mask.paste(mask_layer, (offset_x_shadow, offset_y_shadow))
if shadow_blur > 0:
shadow_mask = shadow_mask.filter(ImageFilter.GaussianBlur(shadow_blur))
shadow_layer = Image.new("RGBA", background.size, shadow_rgb + (255,))
shadow_layer.putalpha(shadow_mask)
background = Image.alpha_composite(background, shadow_layer)
# Apply text layer
text_layer = Image.new("RGBA", background.size, text_rgb + (255,))
text_layer.putalpha(mask_layer)
background = Image.alpha_composite(background, text_layer)
# Apply rotation
if rotation != 0:
fill_color = background_rgb + (255,) if not transparent_background else (0, 0, 0, 0)
background = background.rotate(rotation, expand=False, fillcolor=fill_color)
mask_layer = mask_layer.rotate(rotation, expand=False, fillcolor=0)
# Convert to tensors
image_np = np.array(background).astype(np.float32) / 255.0
mask_np = np.array(mask_layer).astype(np.float32) / 255.0
return (torch.from_numpy(image_np).unsqueeze(0), torch.from_numpy(mask_np).unsqueeze(0))
NODE_CLASS_MAPPINGS = {
"PersianText": PersianText
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PersianText": "Persian Text Generator"
}