Files

782 lines
33 KiB
Python

import vtracer
import os
import time
import folder_paths
import numpy as np
import torch
import fitz
import random
import folder_paths
import potrace
from io import BytesIO
from PIL import Image
from comfy_extras.nodes_images import SVG
from nodes import SaveImage
import re
import xml.etree.ElementTree as ET
def tensor2pil(image):
"""Tensor转PIL图像"""
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
def pil2tensor(image):
"""PIL图像转Tensor"""
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
class TS_ImageQuantize:
"""
图像量化:通过减少图像中的颜色数量来优化矢量转换过程。
"""
@classmethod
def INPUT_TYPES(cls):
"""
定义节点的输入参数。
"""
return {
"required": {
"image": ("IMAGE",),
"colors": ("INT", {"default": 16, "min": 2, "max": 256, "step": 1}),
"dither": (["Clear", "Smooth"], {"default": "Clear"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "quantize_image"
CATEGORY = "💎TOSVG/Tools"
def quantize_image(self, image, colors, dither):
"""
执行图像量化处理。
"""
quantized_images = []
dither_method = Image.Dither.NONE
if dither == "Smooth":
dither_method = Image.Dither.FLOYDSTEINBERG
for i in image:
pil_image = tensor2pil(torch.unsqueeze(i, 0))
quantized_pil = pil_image.convert('RGB').quantize(colors=colors, dither=dither_method)
quantized_pil_rgb = quantized_pil.convert('RGB')
tensor_image = pil2tensor(quantized_pil_rgb)
quantized_images.append(tensor_image.squeeze(0))
if not quantized_images:
return (image,)
return (torch.stack(quantized_images),)
class TS_ImageToSVGStringColor_Vtracer:
"""图像转彩色SVG字符串"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"hierarchical": (["stacked", "cutout"], {"default": "stacked"}),
"mode": (["spline", "polygon", "none"], {"default": "spline"}),
"filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}),
"color_precision": ("INT", {"default": 6, "min": 0, "max": 10, "step": 1}),
"layer_difference": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}),
"corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}),
"length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"max_iterations": ("INT", {"default": 10, "min": 1, "max": 70, "step": 1}),
"splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}),
"path_precision": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "convert_to_svg"
CATEGORY = "💎TOSVG/Convert"
def convert_to_svg(self, image, hierarchical, mode, filter_speckle, color_precision, layer_difference, corner_threshold,
length_threshold, max_iterations, splice_threshold, path_precision):
svg_strings = []
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i)
if _image.mode != 'RGBA':
alpha = Image.new('L', _image.size, 255)
_image.putalpha(alpha)
pixels = list(_image.getdata())
size = _image.size
svg_str = vtracer.convert_pixels_to_svg(
pixels,
size=size,
colormode="color",
hierarchical=hierarchical,
mode=mode,
filter_speckle=filter_speckle,
color_precision=color_precision,
layer_difference=layer_difference,
corner_threshold=corner_threshold,
length_threshold=length_threshold,
max_iterations=max_iterations,
splice_threshold=splice_threshold,
path_precision=path_precision,
)
svg_strings.append(svg_str)
return (svg_strings,)
class TS_ImageToSVGStringBW_Vtracer:
"""图像转黑白SVG字符串"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mode": (["spline", "polygon", "none"], {"default": "spline"}),
"filter_speckle": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}),
"corner_threshold": ("INT", {"default": 60, "min": 0, "max": 180, "step": 1}),
"length_threshold": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"splice_threshold": ("INT", {"default": 45, "min": 0, "max": 180, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "convert_to_svg"
CATEGORY = "💎TOSVG/Convert"
def convert_to_svg(self, image, mode, filter_speckle, corner_threshold, length_threshold, splice_threshold):
svg_strings = []
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i)
if _image.mode != 'RGBA':
alpha = Image.new('L', _image.size, 255)
_image.putalpha(alpha)
pixels = list(_image.getdata())
size = _image.size
svg_str = vtracer.convert_pixels_to_svg(
pixels,
size=size,
colormode="binary",
mode=mode,
filter_speckle=filter_speckle,
corner_threshold=corner_threshold,
length_threshold=length_threshold,
splice_threshold=splice_threshold,
)
svg_strings.append(svg_str)
return (svg_strings,)
class TS_SVGStringToImage:
"""SVG字符串转图像"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"SVG_String": ("STRING", {"forceInput": True})
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "convert_svg_to_image"
CATEGORY = "💎TOSVG/Convert"
def convert_svg_to_image(self, SVG_String):
doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype="svg")
page = doc.load_page(0)
pix = page.get_pixmap()
image_data = pix.tobytes("png")
pil_image = Image.open(BytesIO(image_data)).convert("RGB")
return (pil2tensor(pil_image),)
class TS_SaveSVGString:
"""保存SVG字符串到文件"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"SVG_String": ("STRING", {"forceInput": True}),
"filename_prefix": ("STRING", {"default": "ComfyUI_SVG"}),
},
"optional": {
"append_timestamp": ("BOOLEAN", {"default": True}),
"custom_output_path": ("STRING", {"default": "", "multiline": False}),
}
}
CATEGORY = "💎TOSVG/Tools"
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "save_svg_file"
def generate_unique_filename(self, prefix, timestamp=False):
if timestamp:
timestamp_str = time.strftime("%Y%m%d%H%M%S")
return f"{prefix}_{timestamp_str}.svg"
else:
return f"{prefix}.svg"
def save_svg_file(self, SVG_String, filename_prefix="ComfyUI_SVG", append_timestamp=True, custom_output_path=""):
output_path = custom_output_path if custom_output_path else self.output_dir
os.makedirs(output_path, exist_ok=True)
unique_filename = self.generate_unique_filename(f"{filename_prefix}", append_timestamp)
final_filepath = os.path.join(output_path, unique_filename)
with open(final_filepath, "w") as svg_file:
svg_file.write(SVG_String)
ui_info = {"ui": {"saved_svg": unique_filename, "path": final_filepath}}
return ui_info
class TS_SVGStringPreview(SaveImage):
"""SVG字符串预览"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"SVG_String": ("STRING", {"forceInput": True})
}
}
FUNCTION = "svg_preview"
CATEGORY = "💎TOSVG/Tools"
OUTPUT_NODE = True
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5))
self.compress_level = 4
def svg_preview(self, SVG_String):
doc = fitz.open(stream=SVG_String.encode('utf-8'), filetype="svg")
page = doc.load_page(0)
pix = page.get_pixmap()
image_data = pix.tobytes("png")
pil_image = Image.open(BytesIO(image_data)).convert("RGB")
preview = pil2tensor(pil_image)
return self.save_images(preview, "PointPreview")
class TS_SVGStringToSVGBytesIO:
"""SVG字符串转BytesIO"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"SVG_String": ("STRING", {"forceInput": True}),
}
}
RETURN_TYPES = ("SVG",)
FUNCTION = "convert_string_to_svg"
CATEGORY = "💎TOSVG/Tools"
def convert_string_to_svg(self, SVG_String):
svg_bytes = BytesIO(SVG_String.encode('utf-8'))
return (SVG([svg_bytes]),)
class TS_SVGBytesIOToString:
"""BytesIO转SVG字符串"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"SVG_BytesIO": ("SVG", {"forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "convert_svg_to_string"
CATEGORY = "💎TOSVG/Tools"
def convert_svg_to_string(self, SVG_BytesIO):
if not SVG_BytesIO.data:
return ("",)
svg_bytes = SVG_BytesIO.data[0].getvalue()
svg_string = svg_bytes.decode('utf-8')
return (svg_string,)
class TS_SVGPathSimplify:
"""SVG路径简化"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"SVG_String": ("STRING", {"forceInput": True}),
"tolerance": ("FLOAT", {"default": 5.0, "min": 0.1, "max": 50.0, "step": 0.1}),
"preserve_curves": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "simplify_svg_paths"
CATEGORY = "💎TOSVG/Tools"
def douglas_peucker(self, points, tolerance):
"""Douglas-Peucker算法简化路径点"""
if len(points) <= 2:
return points
# 找到距离起点和终点连线最远的点
max_distance = 0
max_index = 0
start = points[0]
end = points[-1]
if abs(start[0] - end[0]) < 0.01 and abs(start[1] - end[1]) < 0.01:
for i in range(1, len(points) - 1):
distance = ((points[i][0] - start[0]) ** 2 + (points[i][1] - start[1]) ** 2) ** 0.5
if distance > max_distance:
max_distance = distance
max_index = i
else:
for i in range(1, len(points) - 1):
distance = self.point_to_line_distance(points[i], start, end)
if distance > max_distance:
max_distance = distance
max_index = i
if max_distance > tolerance and max_index > 0:
left_part = self.douglas_peucker(points[:max_index + 1], tolerance)
right_part = self.douglas_peucker(points[max_index:], tolerance)
return left_part[:-1] + right_part
else:
return [start, end]
def point_to_line_distance(self, point, line_start, line_end):
"""计算点到直线距离"""
x0, y0 = point
x1, y1 = line_start
x2, y2 = line_end
if x1 == x2 and y1 == y2:
return ((x0 - x1) ** 2 + (y0 - y1) ** 2) ** 0.5
numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
denominator = ((y2 - y1) ** 2 + (x2 - x1) ** 2) ** 0.5
return numerator / denominator if denominator > 0 else 0
def parse_path_commands(self, path_data):
"""解析SVG路径命令"""
commands = re.findall(r'[MmLlHhVvCcSsQqTtAaZz][^MmLlHhVvCcSsQqTtAaZz]*', path_data)
return commands
def extract_points_from_path(self, path_data):
"""提取路径中的坐标点"""
points = []
commands = self.parse_path_commands(path_data)
current_pos = (0, 0)
for command in commands:
cmd_type = command[0]
params = re.findall(r'-?\d*\.?\d+', command[1:])
params = [float(p) for p in params]
if cmd_type in 'Mm':
if len(params) >= 2:
if cmd_type == 'M':
current_pos = (params[0], params[1])
else:
current_pos = (current_pos[0] + params[0], current_pos[1] + params[1])
points.append(current_pos)
elif cmd_type in 'LlHhVv':
if cmd_type in 'Ll':
for i in range(0, len(params), 2):
if i + 1 < len(params):
if cmd_type == 'L':
current_pos = (params[i], params[i + 1])
else:
current_pos = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
points.append(current_pos)
elif cmd_type in 'Hh':
for param in params:
if cmd_type == 'H':
current_pos = (param, current_pos[1])
else:
current_pos = (current_pos[0] + param, current_pos[1])
points.append(current_pos)
elif cmd_type in 'Vv':
for param in params:
if cmd_type == 'V':
current_pos = (current_pos[0], param)
else:
current_pos = (current_pos[0], current_pos[1] + param)
points.append(current_pos)
elif cmd_type in 'CcSsQqTt':
if cmd_type in 'Cc':
for i in range(0, len(params), 6):
if i + 5 < len(params):
if cmd_type == 'C':
control1 = (params[i], params[i + 1])
control2 = (params[i + 2], params[i + 3])
end_point = (params[i + 4], params[i + 5])
points.extend([control1, control2, end_point])
current_pos = end_point
else:
control1 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
control2 = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
end_point = (current_pos[0] + params[i + 4], current_pos[1] + params[i + 5])
points.extend([control1, control2, end_point])
current_pos = end_point
elif cmd_type in 'Ss':
for i in range(0, len(params), 4):
if i + 3 < len(params):
if cmd_type == 'S':
control2 = (params[i], params[i + 1])
end_point = (params[i + 2], params[i + 3])
points.extend([control2, end_point])
current_pos = end_point
else:
control2 = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
points.extend([control2, end_point])
current_pos = end_point
elif cmd_type in 'Qq':
for i in range(0, len(params), 4):
if i + 3 < len(params):
if cmd_type == 'Q':
control = (params[i], params[i + 1])
end_point = (params[i + 2], params[i + 3])
points.extend([control, end_point])
current_pos = end_point
else:
control = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
end_point = (current_pos[0] + params[i + 2], current_pos[1] + params[i + 3])
points.extend([control, end_point])
current_pos = end_point
elif cmd_type in 'Tt':
for i in range(0, len(params), 2):
if i + 1 < len(params):
if cmd_type == 'T':
end_point = (params[i], params[i + 1])
else:
end_point = (current_pos[0] + params[i], current_pos[1] + params[i + 1])
points.append(end_point)
current_pos = end_point
return points
def simplify_path_data(self, path_data, tolerance, preserve_curves, stats):
"""简化路径数据"""
original_points = self.extract_points_from_path(path_data)
stats['original_points'] += len(original_points)
if len(original_points) < 3:
stats['simplified_points'] += len(original_points)
return path_data
if not preserve_curves:
filtered_points = [original_points[0]]
for i in range(1, len(original_points)):
if (abs(original_points[i][0] - filtered_points[-1][0]) > 0.1 or
abs(original_points[i][1] - filtered_points[-1][1]) > 0.1):
filtered_points.append(original_points[i])
original_points = filtered_points
simplified_points = self.douglas_peucker(original_points, tolerance)
stats['simplified_points'] += len(simplified_points)
if preserve_curves and len(simplified_points) >= len(original_points) * 0.9:
stats['simplified_points'] = stats['simplified_points'] - len(simplified_points) + len(original_points)
return path_data
if len(simplified_points) < 2:
return path_data
path_parts = [f"M{simplified_points[0][0]:.1f},{simplified_points[0][1]:.1f}"]
for i in range(1, len(simplified_points)):
x, y = simplified_points[i]
path_parts.append(f"L{x:.1f},{y:.1f}")
if path_data.strip().endswith('Z') or path_data.strip().endswith('z'):
path_parts.append('Z')
return ' '.join(path_parts)
def simplify_svg_paths(self, SVG_String, tolerance, preserve_curves):
"""简化SVG路径"""
effective_tolerance = tolerance
stats = {
'original_points': 0,
'simplified_points': 0,
'paths_processed': 0,
'original_size': len(SVG_String),
'simplified_size': 0
}
try:
root = ET.fromstring(SVG_String)
paths = root.findall('.//{http://www.w3.org/2000/svg}path')
if not paths:
paths = root.findall('.//path')
for path in paths:
if 'd' in path.attrib:
original_data = path.attrib['d']
simplified_data = self.simplify_path_data(original_data, effective_tolerance, preserve_curves, stats)
path.attrib['d'] = simplified_data
stats['paths_processed'] += 1
ET.register_namespace('', 'http://www.w3.org/2000/svg')
simplified_svg = ET.tostring(root, encoding='unicode')
stats['simplified_size'] = len(simplified_svg)
except Exception as e:
simplified_svg = self.simplify_svg_regex(SVG_String, effective_tolerance, preserve_curves, stats)
stats['simplified_size'] = len(simplified_svg)
reduction_ratio = (stats['original_points'] - stats['simplified_points']) / max(stats['original_points'], 1) * 100
print(f"SVG Path Simplified: {reduction_ratio:.1f}% points reduced")
return (simplified_svg,)
def simplify_svg_regex(self, SVG_String, tolerance, preserve_curves, stats):
"""
使用正则表达式方法简化SVG路径(备选方案)
"""
def replace_path(match):
path_data = match.group(1)
simplified = self.simplify_path_data(path_data, tolerance, preserve_curves, stats)
stats['paths_processed'] += 1
return f'd="{simplified}"'
simplified_svg = re.sub(r'd="([^"]*)"', replace_path, SVG_String)
return simplified_svg
class TS_ImageToSVGStringBW_Potracer:
"""Potracer矢量化为SVG"""
turnpolicy_map = {
"minority": potrace.POTRACE_TURNPOLICY_MINORITY,
"black": potrace.POTRACE_TURNPOLICY_BLACK,
"white": potrace.POTRACE_TURNPOLICY_WHITE,
"left": potrace.POTRACE_TURNPOLICY_LEFT,
"right": potrace.POTRACE_TURNPOLICY_RIGHT,
"majority": potrace.POTRACE_TURNPOLICY_MAJORITY,
}
@classmethod
def INPUT_TYPES(cls):
policy_options = list(cls.turnpolicy_map.keys())
return {
"required": {
"image": ("IMAGE",),
"threshold": ("INT", {"default": 128, "min": 0, "max": 255}),
},
"optional": {
"input_foreground": (["White on Black", "Black on White"], {"default": "Black on White"}),
"turnpolicy": (policy_options, {"default": "minority"}),
"turdsize": ("INT", {"default": 2, "min": 0}),
"corner_threshold": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.34, "step": 0.01}),
"zero_sharp_corners": ("BOOLEAN", {"default": False}),
"opttolerance": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01}),
"optimize_curve": ("BOOLEAN", {"default": True}),
"foreground_color": ("STRING", {"widget": "color", "default": "#000000"}),
"background_color": ("STRING", {"widget": "color", "default": "#FFFFFF"}),
"stroke_color": ("STRING", {"widget": "color", "default": "#FF0000"}),
"stroke_width": ("FLOAT", {"default": 0.0, "min": 0.0, "step": 0.5}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "vectorize"
CATEGORY = "💎TOSVG/Convert"
def vectorize(self, image, threshold, turnpolicy, turdsize, corner_threshold, opttolerance,
input_foreground="Black on White", optimize_curve=True,
zero_sharp_corners=False,
foreground_color="#000000", background_color="#FFFFFF",
stroke_color="#FF0000", stroke_width=0.0):
image_np = image.cpu().numpy()
batch_svg_strings = []
for i, single_image_np in enumerate(image_np):
orig_width_temp, orig_height_temp = (single_image_np.shape[1], single_image_np.shape[0]) if single_image_np.ndim >= 2 else (100,100)
svg_data_for_current_image = f'<svg width="{orig_width_temp}" height="{orig_height_temp}"><desc>Error: Processing failed before SVG generation for image {i}</desc></svg>'
try:
pil_img = Image.fromarray((single_image_np * 255).astype(np.uint8))
orig_width, orig_height = pil_img.size
if orig_width <= 0 or orig_height <= 0:
error_svg = f'<svg width="1" height="1"><desc>Error: Invalid image dimensions for image {i}</desc></svg>'
batch_svg_strings.append(error_svg)
continue
threshold_norm = threshold / 255.0
if single_image_np.ndim == 3:
binary_np = single_image_np[:, :, 0] < threshold_norm if single_image_np.shape[2] > 1 else single_image_np[:,:,0] < threshold_norm
elif single_image_np.ndim == 2:
binary_np = single_image_np < threshold_norm
else:
error_svg = f'<svg width="{orig_width}" height="{orig_height}"><desc>Error: Unexpected image dimensions for image {i}</desc></svg>'
batch_svg_strings.append(error_svg)
continue
if input_foreground == "Black on White":
binary_np = ~binary_np
if np.all(binary_np) or not np.any(binary_np):
skipped_svg = f'<svg width="{orig_width}" height="{orig_height}"><desc>Potracer: Skipped blank image {i}</desc></svg>'
batch_svg_strings.append(skipped_svg)
continue
turdsize_int = int(turdsize) if turdsize is not None else 0
policy_arg = self.turnpolicy_map.get(turnpolicy, turnpolicy)
alphamax_value_to_use = 1.34 if zero_sharp_corners else corner_threshold
scale = 1.0
bm = potrace.Bitmap(binary_np)
plist = bm.trace(
turdsize=turdsize_int,
turnpolicy=policy_arg,
alphamax=alphamax_value_to_use,
opticurve=optimize_curve,
opttolerance=opttolerance
)
scaled_width = max(1, round(orig_width * scale))
scaled_height = max(1, round(orig_height * scale))
svg_header = f'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{scaled_width}" height="{scaled_height}" viewBox="0 0 {scaled_width} {scaled_height}">'
svg_footer = "</svg>"
background_rect = ""
bg_color_lower = background_color.lower()
if bg_color_lower != "none" and bg_color_lower != "":
background_rect = f'<rect width="100%" height="100%" fill="{background_color}"/>'
scaled_stroke_width = stroke_width * scale
stroke_attr = f'stroke="{stroke_color}" stroke-width="{scaled_stroke_width}"' if scaled_stroke_width > 0 and stroke_color.lower() != "none" else 'stroke="none"'
fill_attr = f'fill="{foreground_color}"' if foreground_color.lower() != "none" else 'fill="none"'
if fill_attr == 'fill="none"' and stroke_attr == 'stroke="none"':
fill_attr = 'fill="black"'
all_paths_svg_parts = []
if plist:
fill_rule_to_use = "evenodd"
for curve in plist:
if not (hasattr(curve, 'start_point') and hasattr(curve.start_point, 'x') and hasattr(curve.start_point, 'y')):
continue
fs = curve.start_point
all_paths_svg_parts.append(f"M{fs.x * scale:.2f},{fs.y * scale:.2f}")
if not hasattr(curve, 'segments'):
continue
for segment in curve.segments:
valid_segment = True
if not (hasattr(segment, 'is_corner') and hasattr(segment, 'end_point') and hasattr(segment.end_point, 'x') and hasattr(segment.end_point, 'y')):
valid_segment = False
if valid_segment and segment.is_corner:
if not (hasattr(segment, 'c') and hasattr(segment.c, 'x') and hasattr(segment.c, 'y')):
valid_segment = False
else:
c_x = segment.c.x * scale
c_y = segment.c.y * scale
ep_x = segment.end_point.x * scale
ep_y = segment.end_point.y * scale
all_paths_svg_parts.append(f"L{c_x:.2f},{c_y:.2f}L{ep_x:.2f},{ep_y:.2f}")
elif valid_segment:
if not (hasattr(segment, 'c1') and hasattr(segment.c1, 'x') and hasattr(segment.c1, 'y') and \
hasattr(segment, 'c2') and hasattr(segment.c2, 'x') and hasattr(segment.c2, 'y')):
valid_segment = False
else:
c1_x = segment.c1.x * scale; c1_y = segment.c1.y * scale
c2_x = segment.c2.x * scale; c2_y = segment.c2.y * scale
ep_x = segment.end_point.x * scale; ep_y = segment.end_point.y * scale
all_paths_svg_parts.append(f"C{c1_x:.2f},{c1_y:.2f} {c2_x:.2f},{c2_y:.2f} {ep_x:.2f},{ep_y:.2f}")
all_paths_svg_parts.append("Z")
if all_paths_svg_parts:
path_d_attribute = "".join(all_paths_svg_parts)
path_element = f'<path {stroke_attr} {fill_attr} fill-rule="{fill_rule_to_use}" d="{path_d_attribute}"/>'
svg_data_for_current_image = svg_header + background_rect + path_element + svg_footer
else:
svg_data_for_current_image = f'{svg_header}<desc>Potracer: Path data generation failed for image {i}</desc>{svg_footer}'
else:
svg_data_for_current_image = f'{svg_header}<desc>Potracer: No paths found for image {i}</desc>{svg_footer}'
batch_svg_strings.append(svg_data_for_current_image)
except Exception as e:
error_svg_content = f'<svg width="100" height="100"><desc>Error processing image {i}: {type(e).__name__} - {str(e).replace("<", "&lt;").replace(">", "&gt;")}</desc></svg>'
batch_svg_strings.append(error_svg_content)
continue
output_string_joined = "\n".join(batch_svg_strings)
return (output_string_joined,)
NODE_CLASS_MAPPINGS = {
"TS_ImageQuantize": TS_ImageQuantize,
"TS_ImageToSVGStringColor_Vtracer": TS_ImageToSVGStringColor_Vtracer,
"TS_ImageToSVGStringBW_Vtracer": TS_ImageToSVGStringBW_Vtracer,
"TS_SVGStringToImage": TS_SVGStringToImage,
"TS_SaveSVGString": TS_SaveSVGString,
"TS_SVGStringPreview": TS_SVGStringPreview,
"TS_SVGStringToSVGBytesIO": TS_SVGStringToSVGBytesIO,
"TS_SVGBytesIOToString": TS_SVGBytesIOToString,
"TS_SVGPathSimplify": TS_SVGPathSimplify,
"TS_ImageToSVGStringBW_Potracer": TS_ImageToSVGStringBW_Potracer,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TS_ImageQuantize": "Image Quantize",
"TS_ImageToSVGStringColor_Vtracer": "Image to SVG String Color_Vtracer",
"TS_ImageToSVGStringBW_Vtracer": "Image to SVG String BW_Vtracer",
"TS_SVGStringToImage": "SVG String to Image",
"TS_SaveSVGString": "Save SVG String",
"TS_SVGStringPreview": "SVG String Preview",
"TS_SVGStringToSVGBytesIO": "SVG String to SVG BytesIO",
"TS_SVGBytesIOToString": "SVG BytesIO to SVG String",
"TS_SVGPathSimplify": "SVG String Path Simplify",
"TS_ImageToSVGStringBW_Potracer": "Image to SVG String BW_Potracer",
}