-
-
Notifications
You must be signed in to change notification settings - Fork 911
enhance: 全面增强本地渲染策略 #2899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
enhance: 全面增强本地渲染策略 #2899
Conversation
来点人先测测,我改稳定后再通过 |
There was a problem hiding this 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>
请点击 👍 或 👎 对每条评论进行反馈,我将根据反馈改进您的评论,帮助我变得更有用!
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>
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 |
There was a problem hiding this comment.
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()
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 |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议 (代码质量): 合并嵌套的 if 条件 (merge-nested-ifs
)
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
)
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 | |
Explanation
Too much nesting can make code difficult to understand, and this is especiallytrue 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.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议 (代码质量): 合并嵌套的 if 条件 (merge-nested-ifs
)
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
)
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 | |
Explanation
Too much nesting can make code difficult to understand, and this is especiallytrue 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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题 (代码质量): 我们发现以下问题:
- 简化除法表达式 (
simplify-division
) - 将不必要的列表推导替换为生成器表达式 [×4] (
comprehension-to-generator
)
Original comment in English
issue (code-quality): We've found these issues:
- Simplify division expressions (
simplify-division
) - Replace unneeded comprehension with generator [×4] (
comprehension-to-generator
)
deep = 0 | ||
string = False | ||
pre = "" | ||
for i in arg[1:-1] + ",": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题 (代码质量): 我们发现以下问题:
- 使用 f-string 而不是字符串连接 (
use-fstring-for-concatenation
) - 在检查字面量集合的成员资格时使用集合 (
collection-into-set
) - 在控制流跳转后将代码提升到 else 块中 (
reintroduce-else
) - 将 if 语句替换为 if 表达式 (
assign-if-exp
)
Original comment in English
issue (code-quality): We've found these issues:
- Use f-string instead of string concatenation (
use-fstring-for-concatenation
) - Use set when checking membership of a collection of literals (
collection-into-set
) - Lift code into else after jump in control flow (
reintroduce-else
) - Replace if statement with if expression (
assign-if-exp
)
alt = self.ChoiceFont(ch) | ||
if alt: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议 (代码质量): 使用命名表达式简化赋值和条件 (use-named-expression
)
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
)
alt = self.ChoiceFont(ch) | |
if alt: | |
if alt := self.ChoiceFont(ch): |
use_network: bool = True, | ||
): | ||
"""使用默认文转图模板。""" | ||
if use_network: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
问题 (代码质量): 我们发现以下问题:
- 交换 if/else 分支 (
swap-if-else-branches
) - 删除守卫条件后不必要的 else (
remove-unnecessary-else
)
Original comment in English
issue (code-quality): We've found these issues:
- Swap if/else branches (
swap-if-else-branches
) - Remove unnecessary else after guard condition (
remove-unnecessary-else
)
可以先 ruff format 一下 |
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. 列表
5. 链接与图片
链接文本

6. 引用
7. 代码
行内
code
演示。代码块:
8. 分隔线
9. 转义
*不是斜体*
10. 表格
11. 任务列表
12. 删除线
被删除的文字13. 自动链接
https://github.com
14. 脚注
脚注示例1。
15. 定义列表
术语一
: 定义 1a
: 定义 1b
术语二
: 定义 2a
16. 特殊符号
© & < >
17. 内嵌 HTML
红色文字
18. 公式(如果支持)
19. 任务列表嵌套
20. 混合强调
bold italic bold
21. 长引用嵌套
22. 代码块语言标识
"""
@register("astrbot_plugin_pillowmd", "Zhalslar", "...", "...")
class PillowmdtPlugin(Star):
def init(self, context: Context):
super().init(context)
Screenshots or Test Results / 运行截图或测试结果
Compatibility & Breaking Changes / 兼容性与破坏性变更
Checklist / 检查清单
requirements.txt
和pyproject.toml
文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txt
andpyproject.toml
.Sourcery 总结
通过用新的
pillowmd
包替换旧的基础渲染器,引入StyleManager
进行主题处理,更新LocalRenderStrategy
和t2i.Renderer
以利用样式和本地渲染,并改进用于临时图片保存和资产管理的 I/O 工具,从而实现一个全面、可样式化的本地 Markdown 到图片渲染框架。新功能:
StyleManager
以从 YAML 配置中加载和缓存用户定义及内置的渲染主题PillowMdRenderer
,它具有完整的 Markdown 到图片管道,支持背景、装饰、表格、LaTeX、图片、内联扩展和自定义样式参数LocalRenderStrategy
和t2i.Renderer
,使其能够在网络渲染和本地渲染之间进行选择,并向本地渲染器传递样式选项改进:
pillowmd
子包(mixfont
,style
,drawer
,decorates
,mdrenderer
)替换旧的基于元素的MarkdownRenderer
save_temp_img
以使用pathlib
,支持字节和可选的文件命名,自动清理旧文件,并根据图片模式选择 PNG/JPGdownload_image_by_url
以将save_name
传播到临时图片保存文档:
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:
Enhancements:
Documentation:
Footnotes
我是脚注内容。 ↩