Skip to content

Conversation

Zhalslar
Copy link
Contributor

@Zhalslar Zhalslar commented Sep 27, 2025

Motivation / 动机

目前Astrbot的本地t2i渲染相当简陋

Modifications / 改动点

重写渲染逻辑,支持自定义丰富多彩的样式,包括各种元素颜色、装饰、背景、字体立绘、分页、各种Markdown元素

Verification Steps / 验证步骤

加入字体data\t2i_styles\base\fonts\font.ttf后,用插件测试:

from astrbot.api.star import Context, Star, register
from astrbot.api.event import filter
from astrbot.core.platform.astr_message_event import AstrMessageEvent

default_md = """

Markdown 语法速测


1. 标题

H1

H2

H3

2. 段落与硬换行

第一行末尾两个空格
第二行硬换行。

3. 强调

斜体 下斜体
粗体 下粗体
粗斜体 下粗斜体

4. 列表

  • 无序 1
  • 无序 2
    • 嵌套
  1. 有序 1
  2. 有序 2

5. 链接与图片

链接文本
占位图

6. 引用

一级引用

二级引用

7. 代码

行内 code 演示。
代码块:

def hello():
    print("Hello Markdown!")

8. 分隔线


9. 转义

*不是斜体*

10. 表格

名称 数量
苹果 5
香蕉 3

11. 任务列表

  • 已完成任务
  • 待办任务

12. 删除线

被删除的文字

13. 自动链接

https://github.com

14. 脚注

脚注示例1

15. 定义列表

术语一
: 定义 1a
: 定义 1b

术语二
: 定义 2a

16. 特殊符号

© & < >  

17. 内嵌 HTML

红色文字

18. 公式(如果支持)

$$ E = mc^2 $$

19. 任务列表嵌套

  • 父任务
    • 子任务 1
    • 子任务 2

20. 混合强调

bold italic bold

21. 长引用嵌套

一级

二级

三级

22. 代码块语言标识

console.log("Hello, world!");

"""

@register("astrbot_plugin_pillowmd", "Zhalslar", "...", "...")
class PillowmdtPlugin(Star):
def init(self, context: Context):
super().init(context)

@filter.command("pmd")
async def on_pillowmd(self, event: AstrMessageEvent):
    url = await self.text_to_image(default_md, use_network=False)
    yield event.image_result(url)

Screenshots or Test Results / 运行截图或测试结果

download

Compatibility & Breaking Changes / 兼容性与破坏性变更

  • 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
  • 这不是一个破坏性变更。/ This is NOT a breaking change.

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Sourcery 总结

通过用新的 pillowmd 包替换旧的基础渲染器,引入 StyleManager 进行主题处理,更新 LocalRenderStrategyt2i.Renderer 以利用样式和本地渲染,并改进用于临时图片保存和资产管理的 I/O 工具,从而实现一个全面、可样式化的本地 Markdown 到图片渲染框架。

新功能:

  • 添加 StyleManager 以从 YAML 配置中加载和缓存用户定义及内置的渲染主题
  • 引入 PillowMdRenderer,它具有完整的 Markdown 到图片管道,支持背景、装饰、表格、LaTeX、图片、内联扩展和自定义样式参数
  • 增强 LocalRenderStrategyt2i.Renderer,使其能够在网络渲染和本地渲染之间进行选择,并向本地渲染器传递样式选项

改进:

  • pillowmd 子包(mixfont, style, drawer, decorates, mdrenderer)替换旧的基于元素的 MarkdownRenderer
  • 改进 save_temp_img 以使用 pathlib,支持字节和可选的文件命名,自动清理旧文件,并根据图片模式选择 PNG/JPG
  • 重构 download_image_by_url 以将 save_name 传播到临时图片保存
  • 移除简化的本地渲染逻辑,并迁移到样式驱动的渲染系统

文档:

  • 提供一个带有注释样式配置的 YAML 模板(setting.yaml),用于自定义主题
  • pillowmd/__init__.py 中记录额外的 Markdown 语法和自定义图片扩展功能
Original summary in English

Summary by Sourcery

Implement a comprehensive, styleable local Markdown-to-image rendering framework by replacing the old basic renderer with a new pillowmd package, introducing a StyleManager for theme handling, updating LocalRenderStrategy and t2i.Renderer to utilize styles and local rendering, and improving I/O utilities for temporary image saving and asset management.

New Features:

  • Add StyleManager to load and cache user-defined and built-in rendering themes from YAML configurations
  • Introduce PillowMdRenderer with a full Markdown-to-image pipeline supporting backgrounds, decorations, tables, LaTeX, images, inline extensions, and custom style parameters
  • Enhance LocalRenderStrategy and t2i.Renderer to select between network and local rendering and pass style options to local renderer

Enhancements:

  • Replace legacy element-based MarkdownRenderer with pillowmd subpackage (mixfont, style, drawer, decorates, mdrenderer)
  • Improve save_temp_img to use pathlib, support bytes and optional file naming, auto-cleanup old files and choose PNG/JPG based on image mode
  • Refactor download_image_by_url to propagate save_name to temporary image saves
  • Remove simplified local rendering logic and migrate to style-driven rendering system

Documentation:

  • Provide a sample YAML template (setting.yaml) with annotated style configuration for custom themes
  • Document additional Markdown syntax and custom image extension functions in pillowmd/init.py

Footnotes

  1. 我是脚注内容。

@Zhalslar
Copy link
Contributor Author

来点人先测测,我改稳定后再通过

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

大家好 - 我已经审阅了你们的更改 - 这里有一些反馈:

  • LocalRenderStrategy API 现在有多个方法 (render_custom_template, render) 具有不同的参数名称 (return_url, options, style_name 等);考虑统一它们的签名并弃用旧参数,以避免混淆并保持向后兼容性。
  • 此 PR 引入了一个大型重构,涵盖了 IO 实用程序、渲染核心、样式管理和基于 Pillow 的 Markdown 渲染器——将其拆分为更小、更集中的提交或子模块将大大提高可审阅性和可维护性。
  • StyleManager.get_style_from_name 中,不要打印路径并引发原始的 FileNotFoundError,而是提供更清晰的错误消息或回退行为,以改善样式缺失时的开发人员体验。
给 AI 代理的提示
请处理此代码审查中的评论:

## 总体评论
- `LocalRenderStrategy` API 现在有多个方法 (`render_custom_template`, `render`) 具有不同的参数名称 (`return_url`, `options`, `style_name` 等);考虑统一它们的签名并弃用旧参数,以避免混淆并保持向后兼容性。
- 此 PR 引入了一个大型重构,涵盖了 IO 实用程序、渲染核心、样式管理和基于 Pillow 的 Markdown 渲染器——将其拆分为更小、更集中的提交或子模块将大大提高可审阅性和可维护性。
-`StyleManager.get_style_from_name` 中,不要打印路径并引发原始的 `FileNotFoundError`,而是提供更清晰的错误消息或回退行为,以改善样式缺失时的开发人员体验。

## 单个评论

### 评论 1
<location> `astrbot/core/utils/t2i/local_strategy.py:5` </location>
<code_context>
-
+from astrbot.core.utils.t2i import RenderStrategy
+from astrbot.core.utils.t2i.pillowmd.mdrenderer import PillowMdRenderer
+from astrbot.core.utils.t2i.style_manager import StyleManeger

 class LocalRenderStrategy(RenderStrategy):
</code_context>

<issue_to_address>
**小问题 (拼写错误):** 类名有拼写错误:StyleManeger 应该是 StyleManager。

请将 'StyleManeger' 重命名为 'StyleManager' 以保持一致性。

建议的实现:

```python
from astrbot.core.utils.t2i.style_manager import StyleManager

```

```python
        self.style_maneger = StyleManager()

```
</issue_to_address>

### 评论 2
<location> `astrbot/core/utils/t2i/local_strategy.py:14-15` </location>
<code_context>
+        self.style_maneger = StyleManeger()
+        self.renderer = PillowMdRenderer()
+    
     async def render_custom_template(
-        self, tmpl_str: str, tmpl_data: dict, return_url: bool = True
+        self, tmpl_str: str, tmpl_data: dict, options: dict | None = None
     ) -> str:
-        raise NotImplementedError
</code_context>

<issue_to_address>
**建议:** `render_custom_template` 中的 `options` 参数未使用。

如果 `options` 是为了将来使用,请添加一个占位符或 TODO。否则,请将其删除以避免混淆。
</issue_to_address>

### 评论 3
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:239` </location>
<code_context>
+        autoPage = autoPage if autoPage is not None else s.autoPage
+
+        # ========== 1. 预解析:扫描特殊区间 ==========
+        while t.idx < t.textS - 1:
+            t.isImage = False
+            nowObjH = t.nowf.size
</code_context>

<issue_to_address>
**问题 (复杂性):** 考虑重构扫描和绘制循环,将每个 Markdown 令牌处理器提取到单独的函数或类中,以减少重复并扁平化控制流。

```markdown
很明显,这个文件在两个巨大的循环中做了三件非常不同的事情:

  1. “扫描”阶段:标记/测量每个字符和特殊范围(表格、公式、链接等)
  2. 分页/布局阶段:计算换行符、页面、画布大小
  3. “绘制”阶段:与 (1) 非常相似的代码,但进行绘制而不是测量

您可以通过将每个“令牌”/范围处理程序提取到自己的类或方法中,来消除大部分重复并极大地扁平化这两个循环。这里有一个小例子,展示了如何从扫描循环中提取标题解析:

之前(在大的 while … if 链中):
```python
# … inside while t.idx < t.textS:
# ---- 标题 ----
if not t.textMode and i == "#" and not t.codeMode:
    if t.idx+1 < t.textS and text[t.idx+1] == "#":
        if t.idx+2<=t.textS and text[t.idx+2]=="#":
            t.idx+=2; t.nowf = s.font1
        else:
            t.idx+=1; t.nowf = s.font2
    else:
        t.nowf = s.font3
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx +=1
    continue
```

之后——提取到一个方法并在扫描阶段的顶部注册它:
```python
# mdrenderer.py

class MdRenderState:
    # … fields …
    handlers: List[Callable[["MdRenderState"], bool]] = []

    @classmethod
    def create(cls, text, style):
        state = cls(text=text, textS=len(text), nowf=style.mainFont,...)
        state.handlers = [
            parse_heading,
            parse_unordered_list,
            parse_ordered_list,
            parse_blockcode,
            parse_table,
            # …etc… all your other cases
        ]
        return state

async def md_to_image(self, text, style, ...):
    t = MdRenderState.create(text, style)
    while t.idx < t.textS-1:
        t.idx += 1
        i = text[t.idx]
        # first try each handler; if one returns True, it consumed something
        for handler in t.handlers:
            if handler(t, text, style):
                break
        else:
            measure_plain_char(t, i)
```

并在自己的函数中定义每个处理程序:
```python
def parse_heading(t: MdRenderState, text: str, s: MdStyle) -> bool:
    if t.textMode or t.codeMode or text[t.idx] != "#":
        return False
    # consume up to three #, switch font, skip trailing spaces
    count = 1
    while t.idx+count < t.textS and text[t.idx+count]=="#":
        count += 1
    t.nowf = {1:s.font3, 2:s.font2, 3:s.font1}[min(count, 3)]
    t.idx += count
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx += 1
    return True
```

对列表、表格、行内数学、链接、图像等重复此操作。然后“扫描”和“绘制”循环都变为:

  • 一个 `for handler in handlers: if handler(...): break`  
  • 一个默认的 `measure_plain_char(...)``draw_plain_char(...)`  

这:

  - 极大地扁平化了每个循环  
  - 将每个 Markdown 功能局部化到一个小方法中  
  - 在扫描和绘制中重用相同的处理程序列表  
  - 使添加或维护功能成为一处更改  

没有删除现有功能;您只是将代码移动到小的、集中的函数中,并在处理程序注册表中一次性连接它们。
</issue_to_address>

### 评论 4
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:643-669` </location>
<code_context>
            if (
                i == "`"
                and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                and not t.codeMode
                and not t.bMode
            ):
                if not (
                    t.xidx == 1
                    and t.idx + 2 <= t.textS
                    and text[t.idx : t.idx + 3] == "```"
                ):
                    tempIdx = t.idx
                    flag = False
                    while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                        tempIdx += 1
                        if text[tempIdx] == "`":
                            flag = True
                            break
                    if flag or t.bMode2:
                        t.nx += 2
                        if not t.bMode2:
                            t.fontK = t.nowf
                            t.nowf = s.get_gfont(t.nowf)
                        else:
                            t.nowf = t.fontK
                        t.bMode2 = not t.bMode2
                        continue

</code_context>

<issue_to_address>
**建议 (代码质量):** 合并嵌套的 if 条件 ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
            if (
                            i == "`"
                            and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                            and not t.codeMode
                            and not t.bMode
                        ) and not (
                                t.xidx == 1
                                and t.idx + 2 <= t.textS
                                and text[t.idx : t.idx + 3] == "```"
                            ):
                tempIdx = t.idx
                flag = False
                while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                    tempIdx += 1
                    if text[tempIdx] == "`":
                        flag = True
                        break
                if flag or t.bMode2:
                    t.nx += 2
                    if not t.bMode2:
                        t.fontK = t.nowf
                        t.nowf = s.get_gfont(t.nowf)
                    else:
                        t.nowf = t.fontK
                    t.bMode2 = not t.bMode2
                    continue

```

<br/><details><summary>解释</summary>过多的嵌套会使代码难以理解,尤其是在 Python 中,没有括号来帮助区分不同的嵌套级别。

阅读深度嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力在可能的情况下减少嵌套,而两个 `if` 条件可以使用 `and` 组合的情况是一个轻松的胜利。
</details>
</issue_to_address>

### 评论 5
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:843-850` </location>
<code_context>
                if flag:
                    if (len(color) == 7 and color[0] == "#") or color == "None":
                        t.lockColor = None if color == "None" else color # type ignored
                        t.colors.append(
                            {"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
                        )
                        t.idx = tempIdx
                        continue

</code_context>

<issue_to_address>
**建议 (代码质量):** 合并嵌套的 if 条件 ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
                if flag and ((len(color) == 7 and color[0] == "#") or color == "None"):
                    t.lockColor = None if color == "None" else color # type ignored
                    t.colors.append(
                        {"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
                    )
                    t.idx = tempIdx
                    continue

```

<br/><details><summary>解释</summary>过多的嵌套会使代码难以理解,尤其是在 Python 中,没有括号来帮助区分不同的嵌套级别。

阅读深度嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力在可能的情况下减少嵌套,而两个 `if` 条件可以使用 `and` 组合的情况是一个轻松的胜利。
</details>
</issue_to_address>

### 评论 6
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:1541-1573` </location>
<code_context>
            if (
                i == "`"
                and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                and not t.codeMode
                and not t.bMode
            ):
                if not (
                    t.xidx == 1
                    and t.idx + 2 <= t.textS
                    and text[t.idx : t.idx + 3] == "```"
                ):
                    tempIdx = t.idx
                    flag = False
                    while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                        tempIdx += 1
                        if text[tempIdx] == "`":
                            flag = True
                            break
                    if flag or t.bMode2:
                        if not t.bMode2:
                            t.fontK = t.nowf
                            t.nowf = s.get_gfont(t.nowf)
                            fs = t.nowf.size
                        else:
                            fs = t.nowf.size
                            t.nowf = t.fontK
                        t.bMode2 = not t.bMode2
                        zx, zy = s.lb + t.nx, s.ub + t.ny + t.hs[t.yidx - 1]
                        draw.rectangle(
                            (zx, zy - fs - 2, zx + 2, zy), s.insertCodeUnderpainting
                        )
                        t.nx += 2
                        continue

</code_context>

<issue_to_address>
**建议 (代码质量):** 合并嵌套的 if 条件 ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
            if (
                            i == "`"
                            and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                            and not t.codeMode
                            and not t.bMode
                        ) and not (
                                t.xidx == 1
                                and t.idx + 2 <= t.textS
                                and text[t.idx : t.idx + 3] == "```"
                            ):
                tempIdx = t.idx
                flag = False
                while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                    tempIdx += 1
                    if text[tempIdx] == "`":
                        flag = True
                        break
                if flag or t.bMode2:
                    if not t.bMode2:
                        t.fontK = t.nowf
                        t.nowf = s.get_gfont(t.nowf)
                        fs = t.nowf.size
                    else:
                        fs = t.nowf.size
                        t.nowf = t.fontK
                    t.bMode2 = not t.bMode2
                    zx, zy = s.lb + t.nx, s.ub + t.ny + t.hs[t.yidx - 1]
                    draw.rectangle(
                        (zx, zy - fs - 2, zx + 2, zy), s.insertCodeUnderpainting
                    )
                    t.nx += 2
                    continue

```

<br/><details><summary>解释</summary>过多的嵌套会使代码难以理解,尤其是在 Python 中,没有括号来帮助区分不同的嵌套级别。

阅读深度嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力在可能的情况下减少嵌套,而两个 `if` 条件可以使用 `and` 组合的情况是一个很好的胜利。
</details>
</issue_to_address>

### 评论 7
<location> `astrbot/core/utils/io.py:77-84` </location>
<code_context>
def save_temp_img(img: Union[Image.Image, bytes], save_name: str | None = None) -> str:
    """
    保存临时图片:
    - 自动清理超过 12 小时的临时文件
    - 如果提供了 save_name(含扩展名),直接用作文件名;否则按规则自动生成
    - 根据图片模式自动选择保存格式(RGBA -> PNG,其余 -> JPG)
    """
    temp_dir = Path(get_astrbot_data_path()) / "temp"
    temp_dir.mkdir(parents=True, exist_ok=True)

    # 清理超过 12 小时的旧文件
    now = time.time()
    try:
        for f in temp_dir.iterdir():
            if f.is_file() and now - f.stat().st_ctime > 3600 * 12:
                f.unlink(missing_ok=True)
    except Exception as e:
        print(f"清除临时文件失败: {e}")

    # 决定文件名
    if save_name:  # 外部指定了名字
        file_name = save_name
        path = temp_dir / file_name
    else:  # 自动生成
        timestamp = f"{int(now)}_{uuid.uuid4().hex[:8]}"
        if isinstance(img, Image.Image) and img.mode in ("RGBA", "LA"):
            file_name = f"{timestamp}.png"
        else:
            file_name = f"{timestamp}.jpg"
        path = temp_dir / file_name

    # 保存文件
    if isinstance(img, Image.Image):
        if path.suffix.lower() == ".png" or img.mode in ("RGBA", "LA"):
            img.save(path, format="PNG")
        else:
            img.convert("RGB").save(path, format="JPEG", quality=95)
    else:  # bytes
        path.write_bytes(img)

    return str(path)

</code_context>

<issue_to_address>
**问题 (代码质量):** 将重复代码提升到条件语句之外 ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))
</issue_to_address>

### 评论 8
<location> `astrbot/core/utils/io.py:133-138` </location>
<code_context>
async def download_image_by_url(
    url: str, post: bool = False, post_data: dict = None, path=None, save_name=None
) -> str:
    """
    下载图片, 返回 path
    """
    try:
        ssl_context = ssl.create_default_context(
            cafile=certifi.where()
        )  # 使用 certifi 提供的 CA 证书
        connector = aiohttp.TCPConnector(ssl=ssl_context)  # 使用 certifi 的根证书
        async with aiohttp.ClientSession(
            trust_env=True, connector=connector
        ) as session:
            if post:
                async with session.post(url, json=post_data) as resp:
                    if not path:
                        return save_temp_img(await resp.read(), save_name)
                    else:
                        with open(path, "wb") as f:
                            f.write(await resp.read())
                        return path
            else:
                async with session.get(url) as resp:
                    if not path:
                        return save_temp_img(await resp.read(), save_name)
                    else:
                        with open(path, "wb") as f:
                            f.write(await resp.read())
                        return path
    except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
        # 关闭SSL验证
        ssl_context = ssl.create_default_context()
        ssl_context.set_ciphers("DEFAULT")
        async with aiohttp.ClientSession() as session:
            if post:
                async with session.get(url, ssl=ssl_context) as resp:
                    return save_temp_img(await resp.read(), save_name)
            else:
                async with session.get(url, ssl=ssl_context) as resp:
                    return save_temp_img(await resp.read(), save_name)
    except Exception as e:
        raise e

</code_context>

<issue_to_address>
**建议 (代码质量):** 将重复代码提升到条件语句之外 ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))

```suggestion
            async with session.get(url, ssl=ssl_context) as resp:
                return save_temp_img(await resp.read(), save_name)
```
</issue_to_address>

### 评论 9
<location> `astrbot/core/utils/t2i/pillowmd/decorates.py:91` </location>
<code_context>
    def _fill_image(self, bg: Image.Image, img: Image.Image, mode: int) -> None:
        """按模式填充背景"""
        w, h = bg.size
        iw, ih = img.size

        def tile(
            img: Image.Image, dx: int, dy: int, offset_x: int = 0, offset_y: int = 0
        ):
            for y0 in range(offset_y, h, dy):
                for x0 in range(offset_x, w, dx):
                    bg.paste(img, (x0, y0))

        if mode == 0:  # 单图拉伸
            bg.paste(img.resize((w, h)))

        elif mode == 1:  # 九宫格(注意:一般用于 Android NinePatch,这里保留原逻辑)
            iw3, ih3 = iw // 3, ih // 3
            parts = [
                img.crop((i * iw3, j * ih3, (i + 1) * iw3, (j + 1) * ih3))
                for j in range(3)
                for i in range(3)
            ]
            bg.paste(parts[4].resize((w - 2 * iw3, h - 2 * ih3)), (iw3, ih3))
            for i in range(3):
                bg.paste(parts[i].resize((iw3, h - 2 * ih3)), (i * iw3, ih3))
                bg.paste(parts[6 + i].resize((iw3, h - 2 * ih3)), (i * iw3, h - ih3))
            for j in range(3):
                bg.paste(parts[j * 3].resize((w - 2 * iw3, ih3)), (iw3, j * ih3))
                bg.paste(
                    parts[j * 3 + 2].resize((w - 2 * iw3, ih3)), (w - iw3, j * ih3)
                )
            for j in range(3):
                for i in range(3):
                    if (i, j) == (1, 1):
                        continue
                    bg.paste(parts[j * 3 + i], (i * iw3, j * ih3))

        elif mode in (3, 4, 5, 6):  # 平铺模式
            if mode == 3:  # 横向平铺
                img = img.resize((iw, h))
                tile(img, iw, h)
            elif mode == 4:  # 纵向平铺
                img = img.resize((w, ih))
                tile(img, w, ih)
            elif mode == 5:  # 横纵平铺
                tile(img, iw, ih)
            elif mode == 6:  # 居中平铺
                offset_x, offset_y = (w - iw) // 2 % iw, (h - ih) // 2 % ih
                tile(img, iw, ih, offset_x, offset_y)

</code_context>

<issue_to_address>
**建议 (代码质量):** 在检查字面量集合的成员资格时使用集合 ([`collection-into-set`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/collection-into-set/))

```suggestion
        elif mode in {3, 4, 5, 6}:  # 平铺模式
```
</issue_to_address>

### 评论 10
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:78` </location>
<code_context>
def DefaultMdBackGroundDraw(xs: int, ys: int) -> Image.Image:
    image = Image.new("RGBA", (xs, ys), color=(0, 0, 0))

    drawUnder = ImageDrawPro(image)
    for i in range(11):
        drawUnder.rectangle(
            (0, i * int(ys / 10), xs, (i + 1) * int(ys / 10)),
            (52 - 3 * i, 73 - 4 * i, 94 - 2 * i),
        )

    imgUnder2 = Image.new("RGBA", (xs, ys), color=(0, 0, 0, 0))
    drawUnder2 = ImageDrawPro(imgUnder2)
    for i in range(int(xs * ys / 20000) + 1):
        temp = random.randint(1, 5)
        temp1 = random.randint(20, 40)
        temp2 = random.randint(10, 80)
        temp3 = random.randint(0, xs - temp * 4)
        temp4 = random.randint(-50, ys)
        for x in range(3):
            for y in range(temp1):
                if random.randint(1, 2) == 2:
                    continue
                drawUnder2.rectangle(
                    (
                        temp3 + (temp + 2) * x,
                        temp4 + (temp + 2) * y,
                        temp3 + (temp + 2) * x + temp,
                        temp4 + (temp + 2) * y + temp,
                    ),
                    (0, 255, 180, temp2),
                )

    image.alpha_composite(imgUnder2)

    return image

</code_context>

<issue_to_address>
**问题 (代码质量):** 我们发现以下问题:

- 简化除法表达式 [×2] ([`simplify-division`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-division/))
- 将未使用的 for 索引替换为下划线 ([`for-index-underscore`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/for-index-underscore/))
</issue_to_address>

### 评论 11
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:268` </location>
<code_context>
@NewMdExterImageDrawer("balbar")
def MakeBalbar(
    label: str, bal: float, size: int, nowf: MixFont, style = None
) -> Image.Image:
    tempFs = nowf.GetSize(label)
    temp = int(nowf.size / 6) + 1
    halfTemp = int(temp / 2)
    exterImage = Image.new(
        "RGBA",
        (
            tempFs[0] + temp * 3 + size, int(nowf.size + temp * 2)
        ),
        color=(0, 0, 0, 0),
    )
    drawEm = ImageDraw.Draw(exterImage)
    for i in range(11):
        drawEm.rectangle(
            (
                0,
                i * int((exterImage.size[1]) / 10),
                exterImage.size[0],
                (i + 1) * int((exterImage.size[1]) / 10),
            ),
            (40 + 80 - 8 * i, 40 + 80 - 8 * i, 40 + 80 - 8 * i),
        )
    drawEm.text((temp - 1, halfTemp), label, "#00CCCC", nowf.ft_font)
    drawEm.text((temp + 1, halfTemp), label, "#CCFFFF", nowf.ft_font)
    drawEm.text((temp, halfTemp), label, "#33FFFF", nowf.ft_font)
    drawEm.rectangle(
        (temp * 2 + tempFs[0] + size, temp, temp * 2 + tempFs[0] + size, temp + nowf.size),
        (0, 0, 0),
    )
    for i in range(20):
        drawEm.rectangle(
            (
                temp * 2 + tempFs[0] + int(size * bal / 20 * i),
                temp,
                temp * 2 + tempFs[0] + int(size * bal / 20 * (i + 1)),
                temp + nowf.size,
            ),
            (
                int(78 + 78 * ((i / 20) ** 3)),
                int(177 + 177 * ((i / 20) ** 3)),
                int(177 + 177 * ((i / 20) ** 3)),
            ),
        )
        drawEm.rectangle(
            (
                temp * 2 + tempFs[0] + size - int(size * (1 - bal) / 20 * (i + 1)),
                temp,
                temp * 2 + tempFs[0] + size - int(size * (1 - bal) / 20 * i),
                temp + nowf.size,
            ),
            (
                int(177 + 177 * ((i / 20) ** 3)),
                int(21 + 21 * ((i / 20) ** 3)),
                int(21 + 21 * ((i / 20) ** 3)),
            ),
        )
    drawEm.line(
        (
            temp * 2 + tempFs[0] + int(size * bal),
            temp - halfTemp,
            temp * 2 + tempFs[0] + int(size * bal),
            temp + nowf.size + halfTemp,
        ),
        (255, 255, 255),
        5,
    )
    if bal == 0.5:
        drawEm.text(
            (temp * 2 + tempFs[0] + int(size * bal) + 3, halfTemp),
            "+0%",
            (102, 0, 0),
            nowf.ft_font,
        )
    elif bal > 0.5:
        if bal == 1:
            text = "+∞%"
        else:
            text = f"+{round(bal / (1 - bal) * 100 - 100, 2)}%"
        drawEm.text(
            (
                temp * 2 + tempFs[0] + int(size * bal) - nowf.GetSize(text)[0] - 3,
                halfTemp,
            ),
            text,
            (0, 102, 102),
            nowf.ft_font,
        )
    elif bal < 0.5:
        if bal == 0:
            text = "-∞%"
        else:
            text = f"-{round((1 - bal) / bal * 100 - 100, 2)}%"
        drawEm.text(
            (temp * 2 + tempFs[0] + int(size * bal) + 3, halfTemp),
            text,
            (102, 0, 0),
            nowf.ft_font,
        )

    return exterImage

</code_context>

<issue_to_address>
**问题 (代码质量):** 将 if 语句替换为 if 表达式 [×2] ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
</issue_to_address>

### 评论 12
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:310` </location>
<code_context>
@NewMdExterImageDrawer("chabar")
def MakeChabar(
    objs: list[tuple[str, int]],
    xSize: int,
    ySize: int,
    nowf: MixFont,
    style = None,
) -> Image.Image:
    if not style:
        from .style import MdStyle
        style = MdStyle()
    nums = [nowf.GetSize(str(i[1])) for i in objs]
    strs = [nowf.GetSize(i[0]) for i in objs]
    space = int(xSize / (len(objs) * 2 + 1))
    halfSpace = int(space / 2)

    exterImage = Image.new(
        "RGBA",
        (
            int(
                max([i[0] for i in nums])
                + xSize
                + max(strs[-1][0] / 2 - space * 1.5, 0)
            )
            + 5,
            int(ySize + nums[0][1] / 2 + max([i[1] for i in strs])) + 5,
        ),
        color=(0, 0, 0, 0),
    )
    drawEm = ImageDraw.Draw(exterImage)

    lineY = int(ySize + nums[0][1] / 2) - 5
    lineX = int(max([i[0] for i in nums]) + 5)

    maxM = max([i[1] for i in objs])

    for i in range(len(objs)):
        X = space * (1 + i * 2)
        Y = int(ySize * 0.8 * objs[i][1] / maxM)
        color = style.textGradientEndColor
        drawEm.line(
            (lineX, lineY - Y, lineX + X + space, lineY - Y),
            (int(color[0] * 0.6), int(color[1] * 0.6), int(color[2] * 0.6)),
            1,
        )
        drawEm.text(
            (lineX - nums[i][0] - 5, lineY - Y - int(nums[i][1] / 2)),
            str(objs[i][1]),
            style.textColor,
            nowf.ft_font,
        )
        drawEm.text(
            (int(lineX + X + space / 2 - strs[i][0] / 2), lineY + 5),
            objs[i][0],
            style.textColor,
            nowf.ft_font,
        )
        drawEm.rectangle(
            (lineX + X, lineY - Y, lineX + X + space, lineY), style.textGradientEndColor
        )
        drawEm.text(
            (lineX + X + halfSpace - int(nums[i][0] / 2), lineY - Y - nowf.size - 2),
            str(objs[i][1]),
            style.textColor,
            nowf.ft_font,
        )

    drawEm.line((lineX, lineY, lineX + xSize, lineY), style.textColor, 1)
    drawEm.polygon(
        [
            (lineX + xSize, lineY),
            (lineX + xSize - 3, lineY - 3),
            (lineX + xSize - 3, lineY + 3),
        ],
        style.textColor,
    )
    drawEm.line((lineX, lineY - ySize, lineX, lineY), style.textColor, 1)
    drawEm.polygon(
        [
            (lineX, lineY - ySize),
            (lineX - 3, lineY - ySize + 3),
            (lineX + 3, lineY - ySize + 3),
        ],
        style.textColor,
    )

    return exterImage

</code_context>

<issue_to_address>
**问题 (代码质量):** 我们发现以下问题:

- 简化除法表达式 ([`simplify-division`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-division/))
- 将不必要的列表推导替换为生成器表达式 [×4] ([`comprehension-to-generator`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/comprehension-to-generator/))
</issue_to_address>

### 评论 13
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:154` </location>
<code_context>
    @staticmethod
    def get_args(args: str) -> tuple[list[Any], dict[str, Any]]:
        args += ","
        args1 = []
        args2 = {}
        pmt = ""

        def _get_one_arg(arg: str):
            if arg[0] == "[" and arg[-1] == "]":
                args = []
                pmt = ""
                deep = 0
                string = False
                pre = ""
                for i in arg[1:-1] + ",":
                    if i == "]" and not string:
                        deep -= 1
                    if i == '"' and pre != "\\":
                        string = not string

                    if i == "," and deep == 0 and not string:
                        args.append(pmt.strip())
                        pmt = ""
                        pre = ""
                        continue
                    elif i == "[" and not string:
                        deep += 1

                    pmt += i
                    pre = i
                return [_get_one_arg(i) for i in args]
            if arg[0] == '"' and arg[-1] == '"':
                return arg[1:-1]
            if arg in ["True", "true"]:
                return True
            if "." in arg:
                return float(arg)
            return int(arg)

        deep = 0
        pre = ""
        string = False
        for i in args:
            if i == "]" and not string:
                deep -= 1

            if i == '"' and pre != "\\":
                string = not string

            if i == "," and deep == 0 and not string:
                pmt = pmt.strip()
                if (
                    pmt[0]
                    not in [
                        '"',
                        "[",
                    ]
                    and pmt not in ["True", "true", "False", "false"]
                    and not pmt[0].isdigit()
                ):
                    args2[pmt.split("=")[0].strip()] = "=".join(pmt.split("=")[1:]).strip()
                else:
                    args1.append(pmt)
                pmt = ""
                pre = ""
                continue
            elif i == "[" and not string:
                deep += 1

            pmt += i
            pre = i

        args1 = [_get_one_arg(i) for i in args1]
        for key in args2:
            args2[key] = _get_one_arg(args2[key])

        return (args1, args2)

</code_context>

<issue_to_address>
**问题 (代码质量):** 我们发现以下问题:

- 使用 f-string 而不是字符串连接 ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))
- 在检查字面量集合的成员资格时使用集合 ([`collection-into-set`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/collection-into-set/))
- 在控制流跳转后将代码提升到 else 块中 ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- 将 if 语句替换为 if 表达式 ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
</issue_to_address>

### 评论 14
<location> `astrbot/core/utils/t2i/pillowmd/mixfont.py:62-63` </location>
<code_context>
    def GetSize(self, text: str) -> tuple[int, int]:
        """计算文本在字体下的宽高"""
        if not text:
            return 0, 0

        # 优先使用缓存
        cache = self._size_cache.setdefault(self, {})
        if text in cache:
            return cache[text]

        # 确定可用字体
        use_font = self.ft_font
        for ch in text:
            if not self.CheckChar(ch):
                alt = self.ChoiceFont(ch)
                if alt:
                    use_font = alt
                break

        bbox = use_font.getbbox(text)
        size = int(bbox[2] - bbox[0]), int(bbox[3] - bbox[1])
        cache[text] = size
        return size

</code_context>

<issue_to_address>
**建议 (代码质量):** 使用命名表达式简化赋值和条件 ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
                if alt := self.ChoiceFont(ch):
```
</issue_to_address>

### 评论 15
<location> `astrbot/core/utils/t2i/renderer.py:51` </location>
<code_context>
    async def render_t2i(
        self,
        text: str,
        template_name: str | None = None,
        return_url: bool = False,
        use_network: bool = True,
    ):
        """使用默认文转图模板。"""
        if use_network:
            try:
                return await self.network_strategy.render(
                    text, return_url=return_url, template_name=template_name
                )
            except BaseException as e:
                logger.error(
                    f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
                )
                return await self.local_strategy.render(text, template_name)
        else:
            return await self.local_strategy.render(text, template_name)

</code_context>

<issue_to_address>
**问题 (代码质量):** 我们发现以下问题:

- 交换 if/else 分支 ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
- 删除守卫条件后不必要的 else ([`remove-unnecessary-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-else/))
</issue_to_address>

Sourcery 对开源项目免费 - 如果您喜欢我们的评论,请考虑分享 ✨
请点击 👍 或 👎 对每条评论进行反馈,我将根据反馈改进您的评论,帮助我变得更有用!
Original comment in English

Hey there - I've reviewed your changes - here's some feedback:

  • The LocalRenderStrategy API now has multiple methods (render_custom_template, render) with differing parameter names (return_url, options, style_name, etc.); consider unifying their signatures and deprecating old parameters to avoid confusion and maintain backward compatibility.
  • This PR introduces a large refactor spanning IO utilities, rendering core, style management, and the Pillow-based Markdown renderer—splitting these into smaller, focused commits or sub-modules would greatly improve reviewability and maintainability.
  • In StyleManager.get_style_from_name, instead of printing paths and raising a raw FileNotFoundError, provide a clearer error message or fallback behavior to improve developer experience when a style is missing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `LocalRenderStrategy` API now has multiple methods (`render_custom_template`, `render`) with differing parameter names (`return_url`, `options`, `style_name`, etc.); consider unifying their signatures and deprecating old parameters to avoid confusion and maintain backward compatibility.
- This PR introduces a large refactor spanning IO utilities, rendering core, style management, and the Pillow-based Markdown renderer—splitting these into smaller, focused commits or sub-modules would greatly improve reviewability and maintainability.
- In `StyleManager.get_style_from_name`, instead of printing paths and raising a raw FileNotFoundError, provide a clearer error message or fallback behavior to improve developer experience when a style is missing.

## Individual Comments

### Comment 1
<location> `astrbot/core/utils/t2i/local_strategy.py:5` </location>
<code_context>
-
+from astrbot.core.utils.t2i import RenderStrategy
+from astrbot.core.utils.t2i.pillowmd.mdrenderer import PillowMdRenderer
+from astrbot.core.utils.t2i.style_manager import StyleManeger

 class LocalRenderStrategy(RenderStrategy):
</code_context>

<issue_to_address>
**nitpick (typo):** Typo in class name: StyleManeger should be StyleManager.

Please rename 'StyleManeger' to 'StyleManager' to maintain consistency.

Suggested implementation:

```python
from astrbot.core.utils.t2i.style_manager import StyleManager

```

```python
        self.style_maneger = StyleManager()

```
</issue_to_address>

### Comment 2
<location> `astrbot/core/utils/t2i/local_strategy.py:14-15` </location>
<code_context>
+        self.style_maneger = StyleManeger()
+        self.renderer = PillowMdRenderer()
+    
     async def render_custom_template(
-        self, tmpl_str: str, tmpl_data: dict, return_url: bool = True
+        self, tmpl_str: str, tmpl_data: dict, options: dict | None = None
     ) -> str:
-        raise NotImplementedError
</code_context>

<issue_to_address>
**suggestion:** The options parameter is unused in render_custom_template.

If 'options' is meant for future use, add a placeholder or TODO. Otherwise, remove it to prevent confusion.
</issue_to_address>

### Comment 3
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:239` </location>
<code_context>
+        autoPage = autoPage if autoPage is not None else s.autoPage
+
+        # ========== 1. 预解析:扫描特殊区间 ==========
+        while t.idx < t.textS - 1:
+            t.isImage = False
+            nowObjH = t.nowf.size
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the scan and draw loops by extracting each Markdown token handler into separate functions or classes to reduce duplication and flatten control flow.

```markdown
It’s clear this file is doing three very different things in two massive loops:

  1. “Scan” pass: tokenize/measure every character and special span (tables, formulas, links, etc.)
  2. pagination/layout pass: compute line‐breaks, pages, canvas size
  3. “Draw” pass: very similar code to (1), but painting instead of measuring

You can collapse most of the duplication and hugely flatten both loops by extracting each “token”/span handler into its own class or method.  Here’s one small example showing how to pull out heading-parsing from the scan loop:

Before (in the big while … if chain):
```python
# … inside while t.idx < t.textS:
# ---- 标题 ----
if not t.textMode and i == "#" and not t.codeMode:
    if t.idx+1 < t.textS and text[t.idx+1] == "#":
        if t.idx+2<=t.textS and text[t.idx+2]=="#":
            t.idx+=2; t.nowf = s.font1
        else:
            t.idx+=1; t.nowf = s.font2
    else:
        t.nowf = s.font3
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx +=1
    continue
```

After—extract to a method and register it at top of the scan pass:
```python
# mdrenderer.py

class MdRenderState:
    # … fields …
    handlers: List[Callable[['MdRenderState'], bool]] = []

    @classmethod
    def create(cls, text, style):
        state = cls(text=text, textS=len(text), nowf=style.mainFont,...)
        state.handlers = [
            parse_heading,
            parse_unordered_list,
            parse_ordered_list,
            parse_blockcode,
            parse_table,
            # …etc… all your other cases
        ]
        return state

async def md_to_image(self, text, style, ...):
    t = MdRenderState.create(text, style)
    while t.idx < t.textS-1:
        t.idx += 1
        i = text[t.idx]
        # first try each handler; if one returns True, it consumed something
        for handler in t.handlers:
            if handler(t, text, style):
                break
        else:
            measure_plain_char(t, i)
```

And define each handler in its own function:
```python
def parse_heading(t: MdRenderState, text: str, s: MdStyle) -> bool:
    if t.textMode or t.codeMode or text[t.idx] != "#":
        return False
    # consume up to three #, switch font, skip trailing spaces
    count = 1
    while t.idx+count < t.textS and text[t.idx+count]=="#":
        count += 1
    t.nowf = {1:s.font3, 2:s.font2, 3:s.font1}[min(count, 3)]
    t.idx += count
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx += 1
    return True
```

Repeat for lists, tables, inline‐math, links, images, etc.  Then both “scan” and “draw” loops become:

  • A single `for handler in handlers: if handler(...): break`  
  • A default `measure_plain_char(...)` or `draw_plain_char(...)`  

This:

  - dramatically flattens each loop  
  - localizes each Markdown feature to one small method  
  - reuses the same handler list in both scan & draw  
  - makes adding or maintaining a feature a one‐place change  

No existing functionality is removed; you simply move code into small, focused functions and wire them up once in a handler registry.
</issue_to_address>

### Comment 4
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:643-669` </location>
<code_context>
            if (
                i == "`"
                and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                and not t.codeMode
                and not t.bMode
            ):
                if not (
                    t.xidx == 1
                    and t.idx + 2 <= t.textS
                    and text[t.idx : t.idx + 3] == "```"
                ):
                    tempIdx = t.idx
                    flag = False
                    while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                        tempIdx += 1
                        if text[tempIdx] == "`":
                            flag = True
                            break
                    if flag or t.bMode2:
                        t.nx += 2
                        if not t.bMode2:
                            t.fontK = t.nowf
                            t.nowf = s.get_gfont(t.nowf)
                        else:
                            t.nowf = t.fontK
                        t.bMode2 = not t.bMode2
                        continue

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
            if (
                            i == "`"
                            and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                            and not t.codeMode
                            and not t.bMode
                        ) and not (
                                t.xidx == 1
                                and t.idx + 2 <= t.textS
                                and text[t.idx : t.idx + 3] == "```"
                            ):
                tempIdx = t.idx
                flag = False
                while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                    tempIdx += 1
                    if text[tempIdx] == "`":
                        flag = True
                        break
                if flag or t.bMode2:
                    t.nx += 2
                    if not t.bMode2:
                        t.fontK = t.nowf
                        t.nowf = s.get_gfont(t.nowf)
                    else:
                        t.nowf = t.fontK
                    t.bMode2 = not t.bMode2
                    continue

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 5
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:843-850` </location>
<code_context>
                if flag:
                    if (len(color) == 7 and color[0] == "#") or color == "None":
                        t.lockColor = None if color == "None" else color # type ignored
                        t.colors.append(
                            {"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
                        )
                        t.idx = tempIdx
                        continue

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
                if flag and ((len(color) == 7 and color[0] == "#") or color == "None"):
                    t.lockColor = None if color == "None" else color # type ignored
                    t.colors.append(
                        {"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
                    )
                    t.idx = tempIdx
                    continue

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 6
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:1541-1573` </location>
<code_context>
            if (
                i == "`"
                and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                and not t.codeMode
                and not t.bMode
            ):
                if not (
                    t.xidx == 1
                    and t.idx + 2 <= t.textS
                    and text[t.idx : t.idx + 3] == "```"
                ):
                    tempIdx = t.idx
                    flag = False
                    while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                        tempIdx += 1
                        if text[tempIdx] == "`":
                            flag = True
                            break
                    if flag or t.bMode2:
                        if not t.bMode2:
                            t.fontK = t.nowf
                            t.nowf = s.get_gfont(t.nowf)
                            fs = t.nowf.size
                        else:
                            fs = t.nowf.size
                            t.nowf = t.fontK
                        t.bMode2 = not t.bMode2
                        zx, zy = s.lb + t.nx, s.ub + t.ny + t.hs[t.yidx - 1]
                        draw.rectangle(
                            (zx, zy - fs - 2, zx + 2, zy), s.insertCodeUnderpainting
                        )
                        t.nx += 2
                        continue

</code_context>

<issue_to_address>
**suggestion (code-quality):** Merge nested if conditions ([`merge-nested-ifs`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/Python/Default-Rules/merge-nested-ifs))

```suggestion
            if (
                            i == "`"
                            and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
                            and not t.codeMode
                            and not t.bMode
                        ) and not (
                                t.xidx == 1
                                and t.idx + 2 <= t.textS
                                and text[t.idx : t.idx + 3] == "```"
                            ):
                tempIdx = t.idx
                flag = False
                while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
                    tempIdx += 1
                    if text[tempIdx] == "`":
                        flag = True
                        break
                if flag or t.bMode2:
                    if not t.bMode2:
                        t.fontK = t.nowf
                        t.nowf = s.get_gfont(t.nowf)
                        fs = t.nowf.size
                    else:
                        fs = t.nowf.size
                        t.nowf = t.fontK
                    t.bMode2 = not t.bMode2
                    zx, zy = s.lb + t.nx, s.ub + t.ny + t.hs[t.yidx - 1]
                    draw.rectangle(
                        (zx, zy - fs - 2, zx + 2, zy), s.insertCodeUnderpainting
                    )
                    t.nx += 2
                    continue

```

<br/><details><summary>Explanation</summary>Too much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two `if` conditions can be combined using
`and` is an easy win.
</details>
</issue_to_address>

### Comment 7
<location> `astrbot/core/utils/io.py:77-84` </location>
<code_context>
def save_temp_img(img: Union[Image.Image, bytes], save_name: str | None = None) -> str:
    """
    保存临时图片:
    - 自动清理超过 12 小时的临时文件
    - 如果提供了 save_name(含扩展名),直接用作文件名;否则按规则自动生成
    - 根据图片模式自动选择保存格式(RGBA -> PNG,其余 -> JPG)
    """
    temp_dir = Path(get_astrbot_data_path()) / "temp"
    temp_dir.mkdir(parents=True, exist_ok=True)

    # 清理超过 12 小时的旧文件
    now = time.time()
    try:
        for f in temp_dir.iterdir():
            if f.is_file() and now - f.stat().st_ctime > 3600 * 12:
                f.unlink(missing_ok=True)
    except Exception as e:
        print(f"清除临时文件失败: {e}")

    # 决定文件名
    if save_name:  # 外部指定了名字
        file_name = save_name
        path = temp_dir / file_name
    else:  # 自动生成
        timestamp = f"{int(now)}_{uuid.uuid4().hex[:8]}"
        if isinstance(img, Image.Image) and img.mode in ("RGBA", "LA"):
            file_name = f"{timestamp}.png"
        else:
            file_name = f"{timestamp}.jpg"
        path = temp_dir / file_name

    # 保存文件
    if isinstance(img, Image.Image):
        if path.suffix.lower() == ".png" or img.mode in ("RGBA", "LA"):
            img.save(path, format="PNG")
        else:
            img.convert("RGB").save(path, format="JPEG", quality=95)
    else:  # bytes
        path.write_bytes(img)

    return str(path)

</code_context>

<issue_to_address>
**issue (code-quality):** Hoist repeated code outside conditional statement ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))
</issue_to_address>

### Comment 8
<location> `astrbot/core/utils/io.py:133-138` </location>
<code_context>
async def download_image_by_url(
    url: str, post: bool = False, post_data: dict = None, path=None, save_name=None
) -> str:
    """
    下载图片, 返回 path
    """
    try:
        ssl_context = ssl.create_default_context(
            cafile=certifi.where()
        )  # 使用 certifi 提供的 CA 证书
        connector = aiohttp.TCPConnector(ssl=ssl_context)  # 使用 certifi 的根证书
        async with aiohttp.ClientSession(
            trust_env=True, connector=connector
        ) as session:
            if post:
                async with session.post(url, json=post_data) as resp:
                    if not path:
                        return save_temp_img(await resp.read(), save_name)
                    else:
                        with open(path, "wb") as f:
                            f.write(await resp.read())
                        return path
            else:
                async with session.get(url) as resp:
                    if not path:
                        return save_temp_img(await resp.read(), save_name)
                    else:
                        with open(path, "wb") as f:
                            f.write(await resp.read())
                        return path
    except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
        # 关闭SSL验证
        ssl_context = ssl.create_default_context()
        ssl_context.set_ciphers("DEFAULT")
        async with aiohttp.ClientSession() as session:
            if post:
                async with session.get(url, ssl=ssl_context) as resp:
                    return save_temp_img(await resp.read(), save_name)
            else:
                async with session.get(url, ssl=ssl_context) as resp:
                    return save_temp_img(await resp.read(), save_name)
    except Exception as e:
        raise e

</code_context>

<issue_to_address>
**suggestion (code-quality):** Hoist repeated code outside conditional statement ([`hoist-statement-from-if`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/hoist-statement-from-if/))

```suggestion
            async with session.get(url, ssl=ssl_context) as resp:
                return save_temp_img(await resp.read(), save_name)
```
</issue_to_address>

### Comment 9
<location> `astrbot/core/utils/t2i/pillowmd/decorates.py:91` </location>
<code_context>
    def _fill_image(self, bg: Image.Image, img: Image.Image, mode: int) -> None:
        """按模式填充背景"""
        w, h = bg.size
        iw, ih = img.size

        def tile(
            img: Image.Image, dx: int, dy: int, offset_x: int = 0, offset_y: int = 0
        ):
            for y0 in range(offset_y, h, dy):
                for x0 in range(offset_x, w, dx):
                    bg.paste(img, (x0, y0))

        if mode == 0:  # 单图拉伸
            bg.paste(img.resize((w, h)))

        elif mode == 1:  # 九宫格(注意:一般用于 Android NinePatch,这里保留原逻辑)
            iw3, ih3 = iw // 3, ih // 3
            parts = [
                img.crop((i * iw3, j * ih3, (i + 1) * iw3, (j + 1) * ih3))
                for j in range(3)
                for i in range(3)
            ]
            bg.paste(parts[4].resize((w - 2 * iw3, h - 2 * ih3)), (iw3, ih3))
            for i in range(3):
                bg.paste(parts[i].resize((iw3, h - 2 * ih3)), (i * iw3, ih3))
                bg.paste(parts[6 + i].resize((iw3, h - 2 * ih3)), (i * iw3, h - ih3))
            for j in range(3):
                bg.paste(parts[j * 3].resize((w - 2 * iw3, ih3)), (iw3, j * ih3))
                bg.paste(
                    parts[j * 3 + 2].resize((w - 2 * iw3, ih3)), (w - iw3, j * ih3)
                )
            for j in range(3):
                for i in range(3):
                    if (i, j) == (1, 1):
                        continue
                    bg.paste(parts[j * 3 + i], (i * iw3, j * ih3))

        elif mode in (3, 4, 5, 6):  # 平铺模式
            if mode == 3:  # 横向平铺
                img = img.resize((iw, h))
                tile(img, iw, h)
            elif mode == 4:  # 纵向平铺
                img = img.resize((w, ih))
                tile(img, w, ih)
            elif mode == 5:  # 横纵平铺
                tile(img, iw, ih)
            elif mode == 6:  # 居中平铺
                offset_x, offset_y = (w - iw) // 2 % iw, (h - ih) // 2 % ih
                tile(img, iw, ih, offset_x, offset_y)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use set when checking membership of a collection of literals ([`collection-into-set`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/collection-into-set/))

```suggestion
        elif mode in {3, 4, 5, 6}:  # 平铺模式
```
</issue_to_address>

### Comment 10
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:78` </location>
<code_context>
def DefaultMdBackGroundDraw(xs: int, ys: int) -> Image.Image:
    image = Image.new("RGBA", (xs, ys), color=(0, 0, 0))

    drawUnder = ImageDrawPro(image)
    for i in range(11):
        drawUnder.rectangle(
            (0, i * int(ys / 10), xs, (i + 1) * int(ys / 10)),
            (52 - 3 * i, 73 - 4 * i, 94 - 2 * i),
        )

    imgUnder2 = Image.new("RGBA", (xs, ys), color=(0, 0, 0, 0))
    drawUnder2 = ImageDrawPro(imgUnder2)
    for i in range(int(xs * ys / 20000) + 1):
        temp = random.randint(1, 5)
        temp1 = random.randint(20, 40)
        temp2 = random.randint(10, 80)
        temp3 = random.randint(0, xs - temp * 4)
        temp4 = random.randint(-50, ys)
        for x in range(3):
            for y in range(temp1):
                if random.randint(1, 2) == 2:
                    continue
                drawUnder2.rectangle(
                    (
                        temp3 + (temp + 2) * x,
                        temp4 + (temp + 2) * y,
                        temp3 + (temp + 2) * x + temp,
                        temp4 + (temp + 2) * y + temp,
                    ),
                    (0, 255, 180, temp2),
                )

    image.alpha_composite(imgUnder2)

    return image

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Simplify division expressions [×2] ([`simplify-division`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-division/))
- Replace unused for index with underscore ([`for-index-underscore`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/for-index-underscore/))
</issue_to_address>

### Comment 11
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:268` </location>
<code_context>
@NewMdExterImageDrawer("balbar")
def MakeBalbar(
    label: str, bal: float, size: int, nowf: MixFont, style = None
) -> Image.Image:
    tempFs = nowf.GetSize(label)
    temp = int(nowf.size / 6) + 1
    halfTemp = int(temp / 2)
    exterImage = Image.new(
        "RGBA",
        (tempFs[0] + temp * 3 + size, int(nowf.size + temp * 2)),
        color=(0, 0, 0, 0),
    )
    drawEm = ImageDraw.Draw(exterImage)
    for i in range(11):
        drawEm.rectangle(
            (
                0,
                i * int((exterImage.size[1]) / 10),
                exterImage.size[0],
                (i + 1) * int((exterImage.size[1]) / 10),
            ),
            (40 + 80 - 8 * i, 40 + 80 - 8 * i, 40 + 80 - 8 * i),
        )
    drawEm.text((temp - 1, halfTemp), label, "#00CCCC", nowf.ft_font)
    drawEm.text((temp + 1, halfTemp), label, "#CCFFFF", nowf.ft_font)
    drawEm.text((temp, halfTemp), label, "#33FFFF", nowf.ft_font)
    drawEm.rectangle(
        (temp * 2 + tempFs[0], temp, temp * 2 + tempFs[0] + size, temp + nowf.size),
        (0, 0, 0),
    )
    for i in range(20):
        drawEm.rectangle(
            (
                temp * 2 + tempFs[0] + int(size * bal / 20 * i),
                temp,
                temp * 2 + tempFs[0] + int(size * bal / 20 * (i + 1)),
                temp + nowf.size,
            ),
            (
                int(78 + 78 * ((i / 20) ** 3)),
                int(177 + 177 * ((i / 20) ** 3)),
                int(177 + 177 * ((i / 20) ** 3)),
            ),
        )
        drawEm.rectangle(
            (
                temp * 2 + tempFs[0] + size - int(size * (1 - bal) / 20 * (i + 1)),
                temp,
                temp * 2 + tempFs[0] + size - int(size * (1 - bal) / 20 * i),
                temp + nowf.size,
            ),
            (
                int(177 + 177 * ((i / 20) ** 3)),
                int(21 + 21 * ((i / 20) ** 3)),
                int(21 + 21 * ((i / 20) ** 3)),
            ),
        )
    drawEm.line(
        (
            temp * 2 + tempFs[0] + int(size * bal),
            temp - halfTemp,
            temp * 2 + tempFs[0] + int(size * bal),
            temp + nowf.size + halfTemp,
        ),
        (255, 255, 255),
        5,
    )
    if bal == 0.5:
        drawEm.text(
            (temp * 2 + tempFs[0] + int(size * bal) + 3, halfTemp),
            "+0%",
            (102, 0, 0),
            nowf.ft_font,
        )
    elif bal > 0.5:
        if bal == 1:
            text = "+∞%"
        else:
            text = f"+{round(bal / (1 - bal) * 100 - 100, 2)}%"
        drawEm.text(
            (
                temp * 2 + tempFs[0] + int(size * bal) - nowf.GetSize(text)[0] - 3,
                halfTemp,
            ),
            text,
            (0, 102, 102),
            nowf.ft_font,
        )
    elif bal < 0.5:
        if bal == 0:
            text = "-∞%"
        else:
            text = f"-{round((1 - bal) / bal * 100 - 100, 2)}%"
        drawEm.text(
            (temp * 2 + tempFs[0] + int(size * bal) + 3, halfTemp),
            text,
            (102, 0, 0),
            nowf.ft_font,
        )

    return exterImage

</code_context>

<issue_to_address>
**issue (code-quality):** Replace if statement with if expression [×2] ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
</issue_to_address>

### Comment 12
<location> `astrbot/core/utils/t2i/pillowmd/drawer.py:310` </location>
<code_context>
@NewMdExterImageDrawer("chabar")
def MakeChabar(
    objs: list[tuple[str, int]],
    xSize: int,
    ySize: int,
    nowf: MixFont,
    style = None,
) -> Image.Image:
    if not style:
        from .style import MdStyle
        style = MdStyle()
    nums = [nowf.GetSize(str(i[1])) for i in objs]
    strs = [nowf.GetSize(i[0]) for i in objs]
    space = int(xSize / (len(objs) * 2 + 1))
    halfSpace = int(space / 2)

    exterImage = Image.new(
        "RGBA",
        (
            int(
                max([i[0] for i in nums])
                + xSize
                + max(strs[-1][0] / 2 - space * 1.5, 0)
            )
            + 5,
            int(ySize + nums[0][1] / 2 + max([i[1] for i in strs])) + 5,
        ),
        color=(0, 0, 0, 0),
    )
    drawEm = ImageDraw.Draw(exterImage)

    lineY = int(ySize + nums[0][1] / 2) - 5
    lineX = int(max([i[0] for i in nums]) + 5)

    maxM = max([i[1] for i in objs])

    for i in range(len(objs)):
        X = space * (1 + i * 2)
        Y = int(ySize * 0.8 * objs[i][1] / maxM)
        color = style.textGradientEndColor
        drawEm.line(
            (lineX, lineY - Y, lineX + X + space, lineY - Y),
            (int(color[0] * 0.6), int(color[1] * 0.6), int(color[2] * 0.6)),
            1,
        )
        drawEm.text(
            (lineX - nums[i][0] - 5, lineY - Y - int(nums[i][1] / 2)),
            str(objs[i][1]),
            style.textColor,
            nowf.ft_font,
        )
        drawEm.text(
            (int(lineX + X + space / 2 - strs[i][0] / 2), lineY + 5),
            objs[i][0],
            style.textColor,
            nowf.ft_font,
        )
        drawEm.rectangle(
            (lineX + X, lineY - Y, lineX + X + space, lineY), style.textGradientEndColor
        )
        drawEm.text(
            (lineX + X + halfSpace - int(nums[i][0] / 2), lineY - Y - nowf.size - 2),
            str(objs[i][1]),
            style.textColor,
            nowf.ft_font,
        )

    drawEm.line((lineX, lineY, lineX + xSize, lineY), style.textColor, 1)
    drawEm.polygon(
        [
            (lineX + xSize, lineY),
            (lineX + xSize - 3, lineY - 3),
            (lineX + xSize - 3, lineY + 3),
        ],
        style.textColor,
    )
    drawEm.line((lineX, lineY - ySize, lineX, lineY), style.textColor, 1)
    drawEm.polygon(
        [
            (lineX, lineY - ySize),
            (lineX - 3, lineY - ySize + 3),
            (lineX + 3, lineY - ySize + 3),
        ],
        style.textColor,
    )

    return exterImage

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Simplify division expressions ([`simplify-division`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/simplify-division/))
- Replace unneeded comprehension with generator [×4] ([`comprehension-to-generator`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/comprehension-to-generator/))
</issue_to_address>

### Comment 13
<location> `astrbot/core/utils/t2i/pillowmd/mdrenderer.py:154` </location>
<code_context>
    @staticmethod
    def get_args(args: str) -> tuple[list[Any], dict[str, Any]]:
        args += ","
        args1 = []
        args2 = {}
        pmt = ""

        def _get_one_arg(arg: str):
            if arg[0] == "[" and arg[-1] == "]":
                args = []
                pmt = ""
                deep = 0
                string = False
                pre = ""
                for i in arg[1:-1] + ",":
                    if i == "]" and not string:
                        deep -= 1
                    if i == '"' and pre != "\\":
                        string = not string

                    if i == "," and deep == 0 and not string:
                        args.append(pmt.strip())
                        pmt = ""
                        pre = ""
                        continue
                    elif i == "[" and not string:
                        deep += 1

                    pmt += i
                    pre = i
                return [_get_one_arg(i) for i in args]
            if arg[0] == '"' and arg[-1] == '"':
                return arg[1:-1]
            if arg in ["True", "true"]:
                return True
            if "." in arg:
                return float(arg)
            return int(arg)

        deep = 0
        pre = ""
        string = False
        for i in args:
            if i == "]" and not string:
                deep -= 1

            if i == '"' and pre != "\\":
                string = not string

            if i == "," and deep == 0 and not string:
                pmt = pmt.strip()
                if (
                    pmt[0]
                    not in [
                        '"',
                        "[",
                    ]
                    and pmt not in ["True", "true", "False", "false"]
                    and not pmt[0].isdigit()
                ):
                    args2[pmt.split("=")[0].strip()] = "=".join(pmt.split("=")[1:]).strip()
                else:
                    args1.append(pmt)
                pmt = ""
                pre = ""
                continue
            elif i == "[" and not string:
                deep += 1

            pmt += i
            pre = i

        args1 = [_get_one_arg(i) for i in args1]
        for key in args2:
            args2[key] = _get_one_arg(args2[key])

        return (args1, args2)

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Use f-string instead of string concatenation ([`use-fstring-for-concatenation`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-fstring-for-concatenation/))
- Use set when checking membership of a collection of literals ([`collection-into-set`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/collection-into-set/))
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))
</issue_to_address>

### Comment 14
<location> `astrbot/core/utils/t2i/pillowmd/mixfont.py:62-63` </location>
<code_context>
    def GetSize(self, text: str) -> tuple[int, int]:
        """计算文本在字体下的宽高"""
        if not text:
            return 0, 0

        # 优先使用缓存
        cache = self._size_cache.setdefault(self, {})
        if text in cache:
            return cache[text]

        # 确定可用字体
        use_font = self.ft_font
        for ch in text:
            if not self.CheckChar(ch):
                alt = self.ChoiceFont(ch)
                if alt:
                    use_font = alt
                break

        bbox = use_font.getbbox(text)
        size = int(bbox[2] - bbox[0]), int(bbox[3] - bbox[1])
        cache[text] = size
        return size

</code_context>

<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))

```suggestion
                if alt := self.ChoiceFont(ch):
```
</issue_to_address>

### Comment 15
<location> `astrbot/core/utils/t2i/renderer.py:51` </location>
<code_context>
    async def render_t2i(
        self,
        text: str,
        template_name: str | None = None,
        return_url: bool = False,
        use_network: bool = True,
    ):
        """使用默认文转图模板。"""
        if use_network:
            try:
                return await self.network_strategy.render(
                    text, return_url=return_url, template_name=template_name
                )
            except BaseException as e:
                logger.error(
                    f"Failed to render image via AstrBot API: {e}. Falling back to local rendering."
                )
                return await self.local_strategy.render(text, template_name)
        else:
            return await self.local_strategy.render(text, template_name)

</code_context>

<issue_to_address>
**issue (code-quality):** We've found these issues:

- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
- Remove unnecessary else after guard condition ([`remove-unnecessary-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/remove-unnecessary-else/))
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


from astrbot.core.utils.t2i import RenderStrategy
from astrbot.core.utils.t2i.pillowmd.mdrenderer import PillowMdRenderer
from astrbot.core.utils.t2i.style_manager import StyleManeger
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小问题 (拼写错误): 类名有拼写错误:StyleManeger 应该是 StyleManager。

请将 'StyleManeger' 重命名为 'StyleManager' 以保持一致性。

建议的实现:

from astrbot.core.utils.t2i.style_manager import StyleManager
        self.style_maneger = StyleManager()
Original comment in English

nitpick (typo): Typo in class name: StyleManeger should be StyleManager.

Please rename 'StyleManeger' to 'StyleManager' to maintain consistency.

Suggested implementation:

from astrbot.core.utils.t2i.style_manager import StyleManager
        self.style_maneger = StyleManager()

Comment on lines 14 to +15
async def render_custom_template(
self, tmpl_str: str, tmpl_data: dict, return_url: bool = True
self, tmpl_str: str, tmpl_data: dict, options: dict | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议: render_custom_template 中的 options 参数未使用。

如果 options 是为了将来使用,请添加一个占位符或 TODO。否则,请将其删除以避免混淆。

Original comment in English

suggestion: The options parameter is unused in render_custom_template.

If 'options' is meant for future use, add a placeholder or TODO. Otherwise, remove it to prevent confusion.

autoPage = autoPage if autoPage is not None else s.autoPage

# ========== 1. 预解析:扫描特殊区间 ==========
while t.idx < t.textS - 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (复杂性): 考虑重构扫描和绘制循环,将每个 Markdown 令牌处理器提取到单独的函数或类中,以减少重复并扁平化控制流。

很明显,这个文件在两个巨大的循环中做了三件非常不同的事情:

  1. “扫描”阶段:标记/测量每个字符和特殊范围(表格、公式、链接等)
  2. 分页/布局阶段:计算换行符、页面、画布大小
  3. “绘制”阶段:与 (1) 非常相似的代码,但进行绘制而不是测量

您可以通过将每个“令牌”/范围处理程序提取到自己的类或方法中,来消除大部分重复并极大地扁平化这两个循环。这里有一个小例子,展示了如何从扫描循环中提取标题解析:

之前(在大的 while … if 链中):
```python
# … inside while t.idx < t.textS:
# ---- 标题 ----
if not t.textMode and i == "#" and not t.codeMode:
    if t.idx+1 < t.textS and text[t.idx+1] == "#":
        if t.idx+2<=t.textS and text[t.idx+2]=="#":
            t.idx+=2; t.nowf = s.font1
        else:
            t.idx+=1; t.nowf = s.font2
    else:
        t.nowf = s.font3
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx +=1
    continue

之后——提取到一个方法并在扫描阶段的顶部注册它:

# mdrenderer.py

class MdRenderState:
    # … fields …
    handlers: List[Callable[["MdRenderState"], bool]] = []

    @classmethod
    def create(cls, text, style):
        state = cls(text=text, textS=len(text), nowf=style.mainFont,...)
        state.handlers = [
            parse_heading,
            parse_unordered_list,
            parse_ordered_list,
            parse_blockcode,
            parse_table,
            # …etc… all your other cases
        ]
        return state

async def md_to_image(self, text, style, ...):
    t = MdRenderState.create(text, style)
    while t.idx < t.textS-1:
        t.idx += 1
        i = text[t.idx]
        # first try each handler; if one returns True, it consumed something
        for handler in t.handlers:
            if handler(t, text, style):
                break
        else:
            measure_plain_char(t, i)

并在自己的函数中定义每个处理程序:

def parse_heading(t: MdRenderState, text: str, s: MdStyle) -> bool:
    if t.textMode or t.codeMode or text[t.idx] != "#":
        return False
    # consume up to three #, switch font, skip trailing spaces
    count = 1
    while t.idx+count < t.textS and text[t.idx+count]=="#":
        count += 1
    t.nowf = {1:s.font3, 2:s.font2, 3:s.font1}[min(count, 3)]
    t.idx += count
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx += 1
    return True

对列表、表格、行内数学、链接、图像等重复此操作。然后“扫描”和“绘制”循环都变为:

• 一个 for handler in handlers: if handler(...): break
• 一个默认的 measure_plain_char(...)draw_plain_char(...)

这:

  • 极大地扁平化了每个循环
  • 将每个 Markdown 功能局部化到一个小方法中
  • 在扫描和绘制中重用相同的处理程序列表
  • 使添加或维护功能成为一处更改

没有删除现有功能;您只是将代码移动到小的、集中的函数中,并在处理程序注册表中一次性连接它们。

Original comment in English

issue (complexity): Consider refactoring the scan and draw loops by extracting each Markdown token handler into separate functions or classes to reduce duplication and flatten control flow.

It’s clear this file is doing three very different things in two massive loops:

  1. “Scan” pass: tokenize/measure every character and special span (tables, formulas, links, etc.)
  2. pagination/layout pass: compute line‐breaks, pages, canvas size
  3. “Draw” pass: very similar code to (1), but painting instead of measuring

You can collapse most of the duplication and hugely flatten both loops by extracting each “token”/span handler into its own class or method.  Here’s one small example showing how to pull out heading-parsing from the scan loop:

Before (in the big while … if chain):
```python
# … inside while t.idx < t.textS:
# ---- 标题 ----
if not t.textMode and i == "#" and not t.codeMode:
    if t.idx+1 < t.textS and text[t.idx+1] == "#":
        if t.idx+2<=t.textS and text[t.idx+2]=="#":
            t.idx+=2; t.nowf = s.font1
        else:
            t.idx+=1; t.nowf = s.font2
    else:
        t.nowf = s.font3
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx +=1
    continue

After—extract to a method and register it at top of the scan pass:

# mdrenderer.py

class MdRenderState:
    # … fields …
    handlers: List[Callable[['MdRenderState'], bool]] = []

    @classmethod
    def create(cls, text, style):
        state = cls(text=text, textS=len(text), nowf=style.mainFont,...)
        state.handlers = [
            parse_heading,
            parse_unordered_list,
            parse_ordered_list,
            parse_blockcode,
            parse_table,
            # …etc… all your other cases
        ]
        return state

async def md_to_image(self, text, style, ...):
    t = MdRenderState.create(text, style)
    while t.idx < t.textS-1:
        t.idx += 1
        i = text[t.idx]
        # first try each handler; if one returns True, it consumed something
        for handler in t.handlers:
            if handler(t, text, style):
                break
        else:
            measure_plain_char(t, i)

And define each handler in its own function:

def parse_heading(t: MdRenderState, text: str, s: MdStyle) -> bool:
    if t.textMode or t.codeMode or text[t.idx] != "#":
        return False
    # consume up to three #, switch font, skip trailing spaces
    count = 1
    while t.idx+count < t.textS and text[t.idx+count]=="#":
        count += 1
    t.nowf = {1:s.font3, 2:s.font2, 3:s.font1}[min(count, 3)]
    t.idx += count
    while t.idx+1 < t.textS and text[t.idx+1]==" ":
        t.idx += 1
    return True

Repeat for lists, tables, inline‐math, links, images, etc. Then both “scan” and “draw” loops become:

• A single for handler in handlers: if handler(...): break
• A default measure_plain_char(...) or draw_plain_char(...)

This:

  • dramatically flattens each loop
  • localizes each Markdown feature to one small method
  • reuses the same handler list in both scan & draw
  • makes adding or maintaining a feature a one‐place change

No existing functionality is removed; you simply move code into small, focused functions and wire them up once in a handler registry.

Comment on lines +643 to +669
if (
i == "`"
and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
and not t.codeMode
and not t.bMode
):
if not (
t.xidx == 1
and t.idx + 2 <= t.textS
and text[t.idx : t.idx + 3] == "```"
):
tempIdx = t.idx
flag = False
while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
tempIdx += 1
if text[tempIdx] == "`":
flag = True
break
if flag or t.bMode2:
t.nx += 2
if not t.bMode2:
t.fontK = t.nowf
t.nowf = s.get_gfont(t.nowf)
else:
t.nowf = t.fontK
t.bMode2 = not t.bMode2
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 (代码质量): 合并嵌套的 if 条件 (merge-nested-ifs)

Suggested change
if (
i == "`"
and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
and not t.codeMode
and not t.bMode
):
if not (
t.xidx == 1
and t.idx + 2 <= t.textS
and text[t.idx : t.idx + 3] == "```"
):
tempIdx = t.idx
flag = False
while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
tempIdx += 1
if text[tempIdx] == "`":
flag = True
break
if flag or t.bMode2:
t.nx += 2
if not t.bMode2:
t.fontK = t.nowf
t.nowf = s.get_gfont(t.nowf)
else:
t.nowf = t.fontK
t.bMode2 = not t.bMode2
continue
if (
i == "`"
and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
and not t.codeMode
and not t.bMode
) and not (
t.xidx == 1
and t.idx + 2 <= t.textS
and text[t.idx : t.idx + 3] == "```"
):
tempIdx = t.idx
flag = False
while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
tempIdx += 1
if text[tempIdx] == "`":
flag = True
break
if flag or t.bMode2:
t.nx += 2
if not t.bMode2:
t.fontK = t.nowf
t.nowf = s.get_gfont(t.nowf)
else:
t.nowf = t.fontK
t.bMode2 = not t.bMode2
continue


解释过多的嵌套会使代码难以理解,尤其是在 Python 中,没有括号来帮助区分不同的嵌套级别。

阅读深度嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力在可能的情况下减少嵌套,而两个 if 条件可以使用 and 组合的情况是一个轻松的胜利。

Original comment in English

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if (
i == "`"
and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
and not t.codeMode
and not t.bMode
):
if not (
t.xidx == 1
and t.idx + 2 <= t.textS
and text[t.idx : t.idx + 3] == "```"
):
tempIdx = t.idx
flag = False
while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
tempIdx += 1
if text[tempIdx] == "`":
flag = True
break
if flag or t.bMode2:
t.nx += 2
if not t.bMode2:
t.fontK = t.nowf
t.nowf = s.get_gfont(t.nowf)
else:
t.nowf = t.fontK
t.bMode2 = not t.bMode2
continue
if (
i == "`"
and (text[t.idx - 1] != "\\" if t.idx >= 1 else True)
and not t.codeMode
and not t.bMode
) and not (
t.xidx == 1
and t.idx + 2 <= t.textS
and text[t.idx : t.idx + 3] == "```"
):
tempIdx = t.idx
flag = False
while tempIdx < t.textS - 1 and text[tempIdx + 1] != "\n":
tempIdx += 1
if text[tempIdx] == "`":
flag = True
break
if flag or t.bMode2:
t.nx += 2
if not t.bMode2:
t.fontK = t.nowf
t.nowf = s.get_gfont(t.nowf)
else:
t.nowf = t.fontK
t.bMode2 = not t.bMode2
continue


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

Comment on lines +843 to +850
if flag:
if (len(color) == 7 and color[0] == "#") or color == "None":
t.lockColor = None if color == "None" else color # type ignored
t.colors.append(
{"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
)
t.idx = tempIdx
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 (代码质量): 合并嵌套的 if 条件 (merge-nested-ifs)

Suggested change
if flag:
if (len(color) == 7 and color[0] == "#") or color == "None":
t.lockColor = None if color == "None" else color # type ignored
t.colors.append(
{"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
)
t.idx = tempIdx
continue
if flag and ((len(color) == 7 and color[0] == "#") or color == "None"):
t.lockColor = None if color == "None" else color # type ignored
t.colors.append(
{"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
)
t.idx = tempIdx
continue


解释过多的嵌套会使代码难以理解,尤其是在 Python 中,没有括号来帮助区分不同的嵌套级别。

阅读深度嵌套的代码令人困惑,因为您必须跟踪哪些条件与哪些级别相关。因此,我们努力在可能的情况下减少嵌套,而两个 if 条件可以使用 and 组合的情况是一个轻松的胜利。

Original comment in English

suggestion (code-quality): Merge nested if conditions (merge-nested-ifs)

Suggested change
if flag:
if (len(color) == 7 and color[0] == "#") or color == "None":
t.lockColor = None if color == "None" else color # type ignored
t.colors.append(
{"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
)
t.idx = tempIdx
continue
if flag and ((len(color) == 7 and color[0] == "#") or color == "None"):
t.lockColor = None if color == "None" else color # type ignored
t.colors.append(
{"beginIdx": t.idx, "endIdx": tempIdx, "color": t.lockColor}
)
t.idx = tempIdx
continue


ExplanationToo much nesting can make code difficult to understand, and this is especially
true in Python, where there are no brackets to help out with the delineation of
different nesting levels.

Reading deeply nested code is confusing, since you have to keep track of which
conditions relate to which levels. We therefore strive to reduce nesting where
possible, and the situation where two if conditions can be combined using
and is an easy win.

nowf.ft_font,
)
elif bal > 0.5:
if bal == 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (代码质量): 将 if 语句替换为 if 表达式 [×2] (assign-if-exp)

Original comment in English

issue (code-quality): Replace if statement with if expression [×2] (assign-if-exp)

nums = [nowf.GetSize(str(i[1])) for i in objs]
strs = [nowf.GetSize(i[0]) for i in objs]
space = int(xSize / (len(objs) * 2 + 1))
halfSpace = int(space / 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (代码质量): 我们发现以下问题:

Original comment in English

issue (code-quality): We've found these issues:

deep = 0
string = False
pre = ""
for i in arg[1:-1] + ",":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (代码质量): 我们发现以下问题:

Original comment in English

issue (code-quality): We've found these issues:

Comment on lines +62 to +63
alt = self.ChoiceFont(ch)
if alt:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议 (代码质量): 使用命名表达式简化赋值和条件 (use-named-expression)

Suggested change
alt = self.ChoiceFont(ch)
if alt:
if alt := self.ChoiceFont(ch):
Original comment in English

suggestion (code-quality): Use named expression to simplify assignment and conditional (use-named-expression)

Suggested change
alt = self.ChoiceFont(ch)
if alt:
if alt := self.ChoiceFont(ch):

use_network: bool = True,
):
"""使用默认文转图模板。"""
if use_network:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (代码质量): 我们发现以下问题:

Original comment in English

issue (code-quality): We've found these issues:

@anka-afk
Copy link
Member

可以先 ruff format 一下

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants