| import os |
| import re |
| import folder_paths |
| import json |
| from ..libs.utils import AlwaysEqualProxy |
|
|
| class showLoaderSettingsNames: |
| @classmethod |
| def INPUT_TYPES(s): |
| return { |
| "required": { |
| "pipe": ("PIPE_LINE",), |
| }, |
| "hidden": { |
| "unique_id": "UNIQUE_ID", |
| "extra_pnginfo": "EXTRA_PNGINFO", |
| }, |
| } |
|
|
| RETURN_TYPES = ("STRING", "STRING", "STRING",) |
| RETURN_NAMES = ("ckpt_name", "vae_name", "lora_name") |
|
|
| FUNCTION = "notify" |
| OUTPUT_NODE = True |
|
|
| CATEGORY = "EasyUse/Util" |
|
|
| def notify(self, pipe, names=None, unique_id=None, extra_pnginfo=None): |
| if unique_id and extra_pnginfo and "workflow" in extra_pnginfo: |
| workflow = extra_pnginfo["workflow"] |
| node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None) |
| if node: |
| ckpt_name = pipe['loader_settings']['ckpt_name'] if 'ckpt_name' in pipe['loader_settings'] else '' |
| vae_name = pipe['loader_settings']['vae_name'] if 'vae_name' in pipe['loader_settings'] else '' |
| lora_name = pipe['loader_settings']['lora_name'] if 'lora_name' in pipe['loader_settings'] else '' |
|
|
| if ckpt_name: |
| ckpt_name = os.path.basename(os.path.splitext(ckpt_name)[0]) |
| if vae_name: |
| vae_name = os.path.basename(os.path.splitext(vae_name)[0]) |
| if lora_name: |
| lora_name = os.path.basename(os.path.splitext(lora_name)[0]) |
|
|
| names = "ckpt_name: " + ckpt_name + '\n' + "vae_name: " + vae_name + '\n' + "lora_name: " + lora_name |
| node["widgets_values"] = names |
|
|
| return {"ui": {"text": [names]}, "result": (ckpt_name, vae_name, lora_name)} |
|
|
| class sliderControl: |
| @classmethod |
| def INPUT_TYPES(s): |
| return { |
| "required": { |
| "mode": (['ipadapter layer weights'],), |
| "model_type": (['sdxl', 'sd1'],), |
| }, |
| "hidden": { |
| "prompt": "PROMPT", |
| "my_unique_id": "UNIQUE_ID", |
| "extra_pnginfo": "EXTRA_PNGINFO", |
| }, |
| } |
|
|
| RETURN_TYPES = ("STRING",) |
| RETURN_NAMES = ("layer_weights",) |
|
|
| FUNCTION = "control" |
|
|
| CATEGORY = "EasyUse/Util" |
|
|
| def control(self, mode, model_type, prompt=None, my_unique_id=None, extra_pnginfo=None): |
| values = '' |
| if my_unique_id in prompt: |
| if 'values' in prompt[my_unique_id]["inputs"]: |
| values = prompt[my_unique_id]["inputs"]['values'] |
|
|
| return (values,) |
|
|
| class setCkptName: |
| @classmethod |
| def INPUT_TYPES(cls): |
| return {"required": { |
| "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), |
| } |
| } |
|
|
| RETURN_TYPES = (AlwaysEqualProxy('*'),) |
| RETURN_NAMES = ("ckpt_name",) |
| FUNCTION = "set_name" |
| CATEGORY = "EasyUse/Util" |
|
|
| def set_name(self, ckpt_name): |
| return (ckpt_name,) |
|
|
| class setControlName: |
|
|
| @classmethod |
| def INPUT_TYPES(cls): |
| return {"required": { |
| "controlnet_name": (folder_paths.get_filename_list("controlnet"),), |
| } |
| } |
|
|
| RETURN_TYPES = (AlwaysEqualProxy('*'),) |
| RETURN_NAMES = ("controlnet_name",) |
| FUNCTION = "set_name" |
| CATEGORY = "EasyUse/Util" |
|
|
| def set_name(self, controlnet_name): |
| return (controlnet_name,) |
| |
| class setLoraName: |
|
|
| @classmethod |
| def INPUT_TYPES(cls): |
| return {"required": { |
| "lora_name": (folder_paths.get_filename_list("loras"),), |
| } |
| } |
|
|
| RETURN_TYPES = (AlwaysEqualProxy('*'),) |
| RETURN_NAMES = ("lora_name",) |
| FUNCTION = "set_name" |
| CATEGORY = "EasyUse/Util" |
|
|
| def set_name(self, lora_name): |
| 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 = [re.sub(r'\*\*(.+?)\*\*', lambda m: '\x01' + m.group(1) + '\x02', |
| re.sub(r'<br\s*/?>', '\n', c.strip(), flags=re.IGNORECASE)) |
| 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): |
| clean = re.sub('[\x01\x02]', '', text) |
| try: |
| bbox = font.getbbox(clean) |
| return bbox[2] - bbox[0] |
| except Exception: |
| return len(clean) * 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): |
| """换行:先按 \\n 切段,每段再按英文单词边界 / CJK 字符换行。""" |
| if not text: |
| return [''] |
| |
| hard_lines = text.split('\n') |
| if len(hard_lines) > 1: |
| result = [] |
| for hl in hard_lines: |
| result.extend(wrap_text(hl, max_width)) |
| return result if result else [''] |
|
|
| |
| 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: |
| |
| if current: |
| result.append(current) |
| current = token |
| else: |
| |
| 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() |
|
|
| def parse_line_segments(line): |
| """将含 \\x01..\\x02 粗体标记的行拆分为 (text, is_bold) 片段列表。""" |
| result, bold = [], False |
| for part in re.split('([\x01\x02])', line): |
| if part == '\x01': |
| bold = True |
| elif part == '\x02': |
| bold = False |
| elif part: |
| result.append((part, bold)) |
| return result or [('', False)] |
|
|
| def balance_bold_markers(lines): |
| """确保每行粗体标记自成一对:跨行时在行首补开、行尾补关标记。""" |
| result, in_bold = [], False |
| for line in lines: |
| if in_bold: |
| line = '\x01' + line |
| for ch in line: |
| if ch == '\x01': in_bold = True |
| elif ch == '\x02': in_bold = False |
| if in_bold: |
| line = line + '\x02' |
| result.append(line) |
| return result |
|
|
| |
| 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 '' |
| for seg_line in cell.split('\n'): |
| max_w = max(max_w, min(get_text_width(seg_line), 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 = balance_bold_markers(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) |
|
|
| |
| 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) |
|
|
| def render_line(x, y, line, fg): |
| """逐片段渲染一行文字;粗体通过向右偏移 1px 再描一遍来模拟加粗。""" |
| try: |
| text_offset = -font.getbbox(re.sub('[\x01\x02]', '', line) or 'A')[1] |
| except Exception: |
| text_offset = 0 |
| sx = x |
| for seg, is_bold in parse_line_segments(line): |
| draw.text((sx, y + text_offset), seg, font=font, fill=fg) |
| if is_bold: |
| draw.text((sx + 1, y + text_offset), seg, font=font, fill=fg) |
| try: |
| w = font.getbbox(seg)[2] - font.getbbox(seg)[0] |
| except Exception: |
| w = len(seg) * 9 |
| sx += w + (1 if is_bold else 0) |
|
|
| 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: |
| render_line(cx + pad_x, ty, line_text, fg) |
| ty += line_h |
|
|
| row_y += rh + border |
|
|
| |
| 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): |
| |
| if isinstance(table_data, str) and table_data.strip().startswith('{'): |
| try: |
| obj = json.loads(table_data) |
| markdown = obj.get('markdown', '') |
| if not markdown: |
| |
| 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 |
|
|
| |
| 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 = { |
| "easy showLoaderSettingsNames": "Show Loader Settings Names", |
| "easy sliderControl": "Easy Slider Control", |
| "easy ckptNames": "Ckpt Names", |
| "easy controlnetNames": "ControlNet Names", |
| "easy loraNames": "Lora Names", |
| "easy tableEditor": "Table Editor", |
| } |
|
|