Add easy tableEditor

This commit is contained in:
yolain
2026-02-23 11:53:28 +08:00
parent b11c634872
commit 7601371923
12 changed files with 263 additions and 13 deletions
+18 -1
View File
@@ -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": {
+233
View File
@@ -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