Files
rui40000-RUI-Nodes/markdown_image_node.py
rui40000andClaude Opus 4.8 5d3834503e fix: 八方向拆分不再切断角色;全仓库参数补齐中文 tooltip
【修复】角色被格线切断(脚、手杖、飘起的斗篷被削掉)
新增 expand_beyond_cell(默认开启):格子只用来判定「这是哪个方向」,
角色的实际范围由它自身的连通区域决定,按质心归属确保邻居不混入。
实测 8/8 方向的裁剪框边缘 alpha 从 1.00(内容顶到边界=被切断)
降到 0.00,S 方向高度 326→356、E 方向宽度 150→188 把缺的部分找了回来。
代价是需要两遍扫描(先求全序列并集框再提取),耗时 4.7s→14.9s。

【规则】每个参数都必须有中文 tooltip,作为以后的统一约定
全仓库 26 个节点 169 个参数,此前缺 115 个,现已 100% 覆盖。
tooltip 写「怎么调」而不只是「是什么」:给取值区间的实际影响、
推荐值与踩坑提示(如 OpenAI/ZenMux 的地址栏不能带 :// ,
素材拆分节点用于动画序列时顺序会漂移等)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 19:07:01 +08:00

192 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Markdown 转文本图片节点
=======================
输入 Markdown 文本,按阅读器级排版输出一张图片(IMAGE)。
- 尺寸:常用尺寸下拉快选,或选 custom 后用 width/height 精确指定
- 字体:下拉列表来自 Ruinode/font 目录(ttf/otf/ttc),刷新页面即可看到新放入的字体
- 各级字号(正文 + H1~H6)、字间距、行间距、单行字数上限均支持「auto」或手动数值,
auto 会按输出尺寸自动求最合适的值(字号用二分法恰好优雅填满画布)
- 表格、Emoji、代码块、引用、任务清单等完整支持,视觉规范对标 GitHub 阅读器
"""
import numpy as np
import torch
from .mdimg import scan_fonts, MarkdownImageRenderer
from .mdimg.fonts import DEFAULT_FONT_LABEL
_SIZE_PRESETS = {
"custom(使用下方宽高)": None,
"1080×1440 竖版 3:4": (1080, 1440),
"1080×1350 竖版 4:5": (1080, 1350),
"1080×1920 手机 9:16": (1080, 1920),
"1080×1080 方形 1:1": (1080, 1080),
"1920×1080 横屏 16:9": (1920, 1080),
"1280×720 横屏 720P": (1280, 720),
"1200×630 链接封面": (1200, 630),
"2480×3508 A4 纵向": (2480, 3508),
}
_THEMES = {"浅色 light": "light", "深色 dark": "dark", "米色 sepia": "sepia"}
_DEMO_MD = """# Markdown 转图片
支持 **粗体**、*斜体*、`行内代码`、~~删除线~~ 与 [链接](https://example.com)。
## 列表与任务 ✅
- 第一项:支持 Emoji 😀🎉
- 第二项
- 嵌套子项
- [x] 已完成任务
- [ ] 待办任务
## 表格 📊
| 模型 | 输入价 | 输出价 |
|:-----|:------:|-------:|
| GPT 🤖 | $0.2/M | $1.25/M |
| Claude 🧠 | $10/M | $50/M |
> 引用块:优雅的细节,来自阅读器级的排版。
```python
def hello():
print("Hello, Markdown!")
```
"""
def _auto_num(s, lo=None, hi=None):
"""'auto'/空 -> None;否则解析为 float 并夹取范围。"""
t = str(s or "").strip().lower()
if t in ("", "auto", "自动", "none"):
return None
try:
v = float(t)
except ValueError:
return None
if lo is not None:
v = max(lo, v)
if hi is not None:
v = min(hi, v)
return v
class MarkdownToImageNode:
"""Markdown 文本 -> 排版图片。"""
@classmethod
def INPUT_TYPES(cls):
font_labels = list(scan_fonts().keys())
default_font = DEFAULT_FONT_LABEL if DEFAULT_FONT_LABEL in font_labels \
else font_labels[0]
def _sz(desc):
"""auto 类参数:填 auto 走自动推算,填数值则强制指定。"""
return {"default": "auto", "multiline": False,
"tooltip": desc + "\n填 auto 由节点自动推算,填数值则强制指定。"}
return {
"required": {
"markdown": ("STRING", {
"default": _DEMO_MD, "multiline": True,
"tooltip": "Markdown 原文。支持标题、粗斜体、删除线、行内代码、\n"
"代码块、引用、列表、任务清单、表格、链接与彩色 Emoji。\n"
"⚠ 别用 WAS 的「Text Multiline」喂本节点——它会把 # 开头的\n"
"标题行当注释删掉。请用本仓库的「多行文本框(原样输出)」。"
}),
"size_preset": (list(_SIZE_PRESETS.keys()), {
"default": "1080×1440 竖版 3:4",
"tooltip": "输出尺寸预设。选 custom 时才使用下方的宽高,\n"
"选其他预设时下方宽高会被忽略。"
}),
"width": ("INT", {
"default": 1080, "min": 64, "max": 8192, "step": 8,
"tooltip": "自定义宽度,仅在预设选 custom 时生效。"
}),
"height": ("INT", {
"default": 1440, "min": 64, "max": 8192, "step": 8,
"tooltip": "自定义高度,仅在预设选 custom 时生效。\n"
"内容排不下时会自动缩小字号来适配,而不是裁切。"
}),
"font": (font_labels, {
"default": default_font,
"tooltip": "正文字体,列表来自 Ruinode/font 目录(ttf/otf/ttc)。\n"
"放入新字体后刷新页面即出现。同族粗体文件(如 msyhbd)\n"
"会自动配对用于渲染粗体,没有粗体文件时用描边模拟。"
}),
"theme": (list(_THEMES.keys()), {
"default": "浅色 light",
"tooltip": "配色主题:浅色适合分享长图,深色适合代码类内容,\n"
"米色接近纸质阅读观感。"
}),
},
"optional": {
"body_size": ("STRING", _sz("正文字号(像素)。")),
"h1_size": ("STRING", _sz("一级标题字号。auto 时为正文的 2.0 倍。")),
"h2_size": ("STRING", _sz("二级标题字号。auto 时为正文的 1.5 倍。")),
"h3_size": ("STRING", _sz("三级标题字号。auto 时为正文的 1.25 倍。")),
"h4_size": ("STRING", _sz("四级标题字号。auto 时与正文同大。")),
"h5_size": ("STRING", _sz("五级标题字号。auto 时为正文的 0.875 倍。")),
"h6_size": ("STRING", _sz("六级标题字号。auto 时为正文的 0.85 倍。")),
"letter_spacing": ("STRING", _sz("字间距(像素),可为负让字更紧凑。")),
"line_spacing": ("STRING", _sz(
"行距倍数,1.6 左右最接近阅读器观感;\n值越大越松散。")),
"max_chars_per_line": ("STRING", _sz(
"单行最多排多少字,超出即换行。\n调小可让版面更窄、更易读。")),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "render"
CATEGORY = "Rui-Node🐶/文本处理📝"
@classmethod
def VALIDATE_INPUTS(cls, font, size_preset):
"""字体/预设列表可能随目录或版本变化,宽松放行(运行时兜底)。"""
return True
def render(self, markdown, size_preset, width, height, font, theme,
body_size="auto", h1_size="auto", h2_size="auto", h3_size="auto",
h4_size="auto", h5_size="auto", h6_size="auto",
letter_spacing="auto", line_spacing="auto",
max_chars_per_line="auto"):
# ---- 尺寸 ----
preset = _SIZE_PRESETS.get(size_preset)
w, h = preset if preset else (int(width), int(height))
# ---- 字体 ----
fmap = scan_fonts()
font_path = fmap.get(font)
if font_path is None:
font_path = next(iter(fmap.values()))
print(f"[Rui-Node] 字体 '{font}' 不在列表中,已回退 {font_path or '内置默认'}")
# ---- 参数 ----
sizes = {
"body": _auto_num(body_size, 6, 300),
"h1": _auto_num(h1_size, 6, 400), "h2": _auto_num(h2_size, 6, 400),
"h3": _auto_num(h3_size, 6, 400), "h4": _auto_num(h4_size, 6, 400),
"h5": _auto_num(h5_size, 6, 400), "h6": _auto_num(h6_size, 6, 400),
}
renderer = MarkdownImageRenderer(
width=w, height=h, font_path=font_path,
theme=_THEMES.get(theme, "light"),
sizes=sizes,
letter_spacing=_auto_num(letter_spacing, -10, 100),
line_spacing=_auto_num(line_spacing, 0.8, 4.0),
max_chars=_auto_num(max_chars_per_line, 1, 1000),
)
img = renderer.render(markdown)
arr = np.asarray(img, dtype=np.float32) / 255.0
tensor = torch.from_numpy(arr).unsqueeze(0) # BHWC
return (tensor,)
NODE_CLASS_MAPPINGS = {
"MarkdownToImage": MarkdownToImageNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"MarkdownToImage": "Markdown转图片 / Markdown To Image",
}