Add easy tableEditor
This commit is contained in:
Submodule ComfyUI-Easy-Use-Frontend updated: 09233fbfb5...64ad07ee04
@@ -6088,7 +6088,7 @@
|
||||
"display_name": "ckpt名称列表",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "模型名称"
|
||||
"name": "模型名称"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -6110,6 +6110,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy tableEditor": {
|
||||
"display_name": "表格编辑器",
|
||||
"description": "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串及渲染图像。",
|
||||
"inputs": {
|
||||
"table_data": {
|
||||
"name": "表格数据"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Markdown"
|
||||
},
|
||||
"1": {
|
||||
"name": "图像"
|
||||
}
|
||||
}
|
||||
},
|
||||
"easy string": {
|
||||
"display_name": "字符串",
|
||||
"inputs": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import folder_paths
|
||||
import json
|
||||
from ..libs.utils import AlwaysEqualProxy
|
||||
|
||||
class showLoaderSettingsNames:
|
||||
@@ -125,12 +127,242 @@ class setLoraName:
|
||||
return (lora_name,)
|
||||
|
||||
|
||||
def _markdown_table_to_image(markdown: str, font_path: str):
|
||||
"""将 Markdown 表格字符串渲染为 PIL.Image(RGB),支持单元格自动换行。"""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# 解析行,过滤分隔行(如 |---|---|)
|
||||
lines = [l for l in (markdown or '').strip().splitlines() if l.strip()]
|
||||
table_rows = []
|
||||
for line in lines:
|
||||
if re.match(r'^\|[\s\-:|]+\|$', line.strip()):
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip().strip('|').split('|')]
|
||||
table_rows.append(cells)
|
||||
|
||||
if not table_rows:
|
||||
return Image.new("RGB", (400, 80), (255, 255, 255))
|
||||
|
||||
num_cols = max(len(r) for r in table_rows)
|
||||
table_rows = [r + [''] * (num_cols - len(r)) for r in table_rows]
|
||||
|
||||
# 加载字体
|
||||
font_size = 16
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, font_size)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
pad_x, pad_y = 14, 10
|
||||
border = 1
|
||||
max_cell_text_width = 200 # 单元格文字区域最大宽度(像素)
|
||||
|
||||
def get_text_width(text):
|
||||
try:
|
||||
bbox = font.getbbox(text)
|
||||
return bbox[2] - bbox[0]
|
||||
except Exception:
|
||||
return len(text) * 9
|
||||
|
||||
def get_line_height():
|
||||
try:
|
||||
bbox = font.getbbox('Ag\u4e2d')
|
||||
return bbox[3] - bbox[1]
|
||||
except Exception:
|
||||
return font_size + 2
|
||||
|
||||
def wrap_text(text, max_width):
|
||||
"""换行:英文按单词边界换行,CJK 字符逐字换行。"""
|
||||
if not text:
|
||||
return ['']
|
||||
|
||||
# 将文本拆分为:CJK 单字符 / 空白序列 / 非CJK非空白序列(英文单词/标点等)
|
||||
tokens = re.findall(
|
||||
r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]'
|
||||
r'|[ \t]+'
|
||||
r'|[^ \t\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+',
|
||||
text
|
||||
)
|
||||
|
||||
result, current = [], ''
|
||||
for token in tokens:
|
||||
is_space = token.strip() == ''
|
||||
test = current + token
|
||||
if get_text_width(test) <= max_width:
|
||||
if is_space and not current:
|
||||
continue # 跳过行首空格
|
||||
current = test
|
||||
else:
|
||||
if is_space:
|
||||
# 空白处换行,丢弃该空白
|
||||
if current:
|
||||
result.append(current)
|
||||
current = ''
|
||||
elif get_text_width(token) <= max_width:
|
||||
# 整个 token 能放一行,整体移到下一行
|
||||
if current:
|
||||
result.append(current)
|
||||
current = token
|
||||
else:
|
||||
# token 本身超宽(极长单词),逐字符强拆
|
||||
for char in token:
|
||||
if get_text_width(current + char) <= max_width:
|
||||
current += char
|
||||
else:
|
||||
if current:
|
||||
result.append(current)
|
||||
current = char
|
||||
if current:
|
||||
result.append(current)
|
||||
return result if result else ['']
|
||||
|
||||
line_h = get_line_height()
|
||||
|
||||
# 第一遍:计算各列宽度(不超过 max_cell_text_width)
|
||||
col_text_widths = []
|
||||
for col_idx in range(num_cols):
|
||||
max_w = 0
|
||||
for row in table_rows:
|
||||
cell = row[col_idx] if col_idx < len(row) else ''
|
||||
max_w = max(max_w, min(get_text_width(cell), max_cell_text_width))
|
||||
col_text_widths.append(max_w)
|
||||
col_widths = [w + pad_x * 2 for w in col_text_widths]
|
||||
|
||||
# 第二遍:对每行每格换行,计算各行高度
|
||||
wrapped_rows = []
|
||||
row_heights = []
|
||||
for row in table_rows:
|
||||
wrapped_cells = []
|
||||
max_lines = 1
|
||||
for col_idx in range(num_cols):
|
||||
cell = row[col_idx] if col_idx < len(row) else ''
|
||||
wrapped = wrap_text(cell, col_text_widths[col_idx])
|
||||
wrapped_cells.append(wrapped)
|
||||
max_lines = max(max_lines, len(wrapped))
|
||||
wrapped_rows.append(wrapped_cells)
|
||||
row_heights.append(max_lines * line_h + pad_y * 2)
|
||||
|
||||
# 每列左边缘 x 坐标(每列前留 1px 边框)
|
||||
col_x = [border]
|
||||
for cw in col_widths:
|
||||
col_x.append(col_x[-1] + cw + border)
|
||||
|
||||
total_width = col_x[-1]
|
||||
total_height = border + sum(rh + border for rh in row_heights)
|
||||
|
||||
# 配色
|
||||
header_bg = (52, 73, 94)
|
||||
header_fg = (255, 255, 255)
|
||||
even_bg = (248, 249, 252)
|
||||
odd_bg = (255, 255, 255)
|
||||
border_color = (180, 185, 195)
|
||||
text_color = (50, 54, 62)
|
||||
|
||||
# 以边框色填充整张图,格线自然显现
|
||||
img = Image.new("RGB", (total_width, total_height), border_color)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
row_y = border
|
||||
for row_idx, (wrapped_cells, rh) in enumerate(zip(wrapped_rows, row_heights)):
|
||||
is_header = row_idx == 0
|
||||
bg = header_bg if is_header else (odd_bg if row_idx % 2 == 1 else even_bg)
|
||||
fg = header_fg if is_header else text_color
|
||||
|
||||
for col_idx in range(num_cols):
|
||||
cx, cw = col_x[col_idx], col_widths[col_idx]
|
||||
# 填充单元格背景
|
||||
draw.rectangle([cx, row_y, cx + cw - 1, row_y + rh - 1], fill=bg)
|
||||
|
||||
cell_lines = wrapped_cells[col_idx] if col_idx < len(wrapped_cells) else ['']
|
||||
total_text_h = len(cell_lines) * line_h
|
||||
ty = row_y + (rh - total_text_h) // 2 # 垂直居中起点
|
||||
for line_text in cell_lines:
|
||||
try:
|
||||
bbox = font.getbbox(line_text)
|
||||
text_offset = -bbox[1] # 修正字体上方留白
|
||||
except Exception:
|
||||
text_offset = 0
|
||||
draw.text((cx + pad_x, ty + text_offset), line_text, font=font, fill=fg)
|
||||
ty += line_h
|
||||
|
||||
row_y += rh + border
|
||||
|
||||
# 最长边不小于 1280,等比放大
|
||||
min_long_side = 1280
|
||||
long_side = max(img.width, img.height)
|
||||
if long_side < min_long_side:
|
||||
scale = min_long_side / long_side
|
||||
new_w = round(img.width * scale)
|
||||
new_h = round(img.height * scale)
|
||||
img = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
class tableEditor:
|
||||
"""表格编辑器节点 —— 通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 字符串。"""
|
||||
|
||||
CATEGORY = "EasyUse/Util"
|
||||
|
||||
RETURN_TYPES = ("STRING", "IMAGE")
|
||||
RETURN_NAMES = ("markdown", "image")
|
||||
FUNCTION = "execute"
|
||||
|
||||
DESCRIPTION = "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串。"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"table_data": ("EASY_TABLE_EDITOR",),
|
||||
},
|
||||
}
|
||||
|
||||
def execute(self, table_data):
|
||||
# 表格数据可能是纯 Markdown 字符串,也可能是序列化后的 JSON
|
||||
if isinstance(table_data, str) and table_data.strip().startswith('{'):
|
||||
try:
|
||||
obj = json.loads(table_data)
|
||||
markdown = obj.get('markdown', '')
|
||||
if not markdown:
|
||||
# 重新从 headers/rows 生成
|
||||
headers = obj.get('headers', [])
|
||||
rows = obj.get('rows', [])
|
||||
col_widths = [max(len(str(h)), 3) for h in headers]
|
||||
for row in rows:
|
||||
for i, cell in enumerate(row):
|
||||
if i < len(col_widths):
|
||||
col_widths[i] = max(col_widths[i], len(str(cell)))
|
||||
header_line = '| ' + ' | '.join(str(h).ljust(col_widths[i]) for i, h in enumerate(headers)) + ' |'
|
||||
sep_line = '| ' + ' | '.join('-' * w for w in col_widths) + ' |'
|
||||
row_lines = [
|
||||
'| ' + ' | '.join(str(row[i] if i < len(row) else '').ljust(col_widths[i]) for i in range(len(headers))) + ' |'
|
||||
for row in rows
|
||||
]
|
||||
markdown = '\n'.join([header_line, sep_line] + row_lines)
|
||||
except Exception:
|
||||
markdown = table_data
|
||||
else:
|
||||
markdown = table_data
|
||||
|
||||
# 将 Markdown 表格渲染为图像
|
||||
font_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
||||
'resources', 'wenquan.ttf'
|
||||
)
|
||||
from ..libs.image import pil2tensor
|
||||
img_tensor = pil2tensor(_markdown_table_to_image(markdown, font_path).convert("RGB"))
|
||||
|
||||
return (markdown, img_tensor)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"easy showLoaderSettingsNames": showLoaderSettingsNames,
|
||||
"easy sliderControl": sliderControl,
|
||||
"easy ckptNames": setCkptName,
|
||||
"easy controlnetNames": setControlName,
|
||||
"easy loraNames": setLoraName,
|
||||
"easy tableEditor": tableEditor,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -139,4 +371,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"easy ckptNames": "Ckpt Names",
|
||||
"easy controlnetNames": "ControlNet Names",
|
||||
"easy loraNames": "Lora Names",
|
||||
"easy tableEditor": "Table Editor",
|
||||
}
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user