AstrBot 3.5.6__py3-none-any.whl → 4.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. astrbot/api/__init__.py +16 -4
  2. astrbot/api/all.py +2 -1
  3. astrbot/api/event/__init__.py +5 -6
  4. astrbot/api/event/filter/__init__.py +37 -34
  5. astrbot/api/platform/__init__.py +7 -8
  6. astrbot/api/provider/__init__.py +8 -7
  7. astrbot/api/star/__init__.py +3 -4
  8. astrbot/api/util/__init__.py +2 -2
  9. astrbot/cli/__init__.py +1 -0
  10. astrbot/cli/__main__.py +18 -197
  11. astrbot/cli/commands/__init__.py +6 -0
  12. astrbot/cli/commands/cmd_conf.py +209 -0
  13. astrbot/cli/commands/cmd_init.py +56 -0
  14. astrbot/cli/commands/cmd_plug.py +245 -0
  15. astrbot/cli/commands/cmd_run.py +62 -0
  16. astrbot/cli/utils/__init__.py +18 -0
  17. astrbot/cli/utils/basic.py +76 -0
  18. astrbot/cli/utils/plugin.py +246 -0
  19. astrbot/cli/utils/version_comparator.py +90 -0
  20. astrbot/core/__init__.py +17 -19
  21. astrbot/core/agent/agent.py +14 -0
  22. astrbot/core/agent/handoff.py +38 -0
  23. astrbot/core/agent/hooks.py +30 -0
  24. astrbot/core/agent/mcp_client.py +385 -0
  25. astrbot/core/agent/message.py +175 -0
  26. astrbot/core/agent/response.py +14 -0
  27. astrbot/core/agent/run_context.py +22 -0
  28. astrbot/core/agent/runners/__init__.py +3 -0
  29. astrbot/core/agent/runners/base.py +65 -0
  30. astrbot/core/agent/runners/coze/coze_agent_runner.py +367 -0
  31. astrbot/core/agent/runners/coze/coze_api_client.py +324 -0
  32. astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +403 -0
  33. astrbot/core/agent/runners/dify/dify_agent_runner.py +336 -0
  34. astrbot/core/agent/runners/dify/dify_api_client.py +195 -0
  35. astrbot/core/agent/runners/tool_loop_agent_runner.py +400 -0
  36. astrbot/core/agent/tool.py +285 -0
  37. astrbot/core/agent/tool_executor.py +17 -0
  38. astrbot/core/astr_agent_context.py +19 -0
  39. astrbot/core/astr_agent_hooks.py +36 -0
  40. astrbot/core/astr_agent_run_util.py +80 -0
  41. astrbot/core/astr_agent_tool_exec.py +246 -0
  42. astrbot/core/astrbot_config_mgr.py +275 -0
  43. astrbot/core/config/__init__.py +2 -2
  44. astrbot/core/config/astrbot_config.py +60 -20
  45. astrbot/core/config/default.py +1972 -453
  46. astrbot/core/config/i18n_utils.py +110 -0
  47. astrbot/core/conversation_mgr.py +285 -75
  48. astrbot/core/core_lifecycle.py +167 -62
  49. astrbot/core/db/__init__.py +305 -102
  50. astrbot/core/db/migration/helper.py +69 -0
  51. astrbot/core/db/migration/migra_3_to_4.py +357 -0
  52. astrbot/core/db/migration/migra_45_to_46.py +44 -0
  53. astrbot/core/db/migration/migra_webchat_session.py +131 -0
  54. astrbot/core/db/migration/shared_preferences_v3.py +48 -0
  55. astrbot/core/db/migration/sqlite_v3.py +497 -0
  56. astrbot/core/db/po.py +259 -55
  57. astrbot/core/db/sqlite.py +773 -528
  58. astrbot/core/db/vec_db/base.py +73 -0
  59. astrbot/core/db/vec_db/faiss_impl/__init__.py +3 -0
  60. astrbot/core/db/vec_db/faiss_impl/document_storage.py +392 -0
  61. astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +93 -0
  62. astrbot/core/db/vec_db/faiss_impl/sqlite_init.sql +17 -0
  63. astrbot/core/db/vec_db/faiss_impl/vec_db.py +204 -0
  64. astrbot/core/event_bus.py +26 -22
  65. astrbot/core/exceptions.py +9 -0
  66. astrbot/core/file_token_service.py +98 -0
  67. astrbot/core/initial_loader.py +19 -10
  68. astrbot/core/knowledge_base/chunking/__init__.py +9 -0
  69. astrbot/core/knowledge_base/chunking/base.py +25 -0
  70. astrbot/core/knowledge_base/chunking/fixed_size.py +59 -0
  71. astrbot/core/knowledge_base/chunking/recursive.py +161 -0
  72. astrbot/core/knowledge_base/kb_db_sqlite.py +301 -0
  73. astrbot/core/knowledge_base/kb_helper.py +642 -0
  74. astrbot/core/knowledge_base/kb_mgr.py +330 -0
  75. astrbot/core/knowledge_base/models.py +120 -0
  76. astrbot/core/knowledge_base/parsers/__init__.py +13 -0
  77. astrbot/core/knowledge_base/parsers/base.py +51 -0
  78. astrbot/core/knowledge_base/parsers/markitdown_parser.py +26 -0
  79. astrbot/core/knowledge_base/parsers/pdf_parser.py +101 -0
  80. astrbot/core/knowledge_base/parsers/text_parser.py +42 -0
  81. astrbot/core/knowledge_base/parsers/url_parser.py +103 -0
  82. astrbot/core/knowledge_base/parsers/util.py +13 -0
  83. astrbot/core/knowledge_base/prompts.py +65 -0
  84. astrbot/core/knowledge_base/retrieval/__init__.py +14 -0
  85. astrbot/core/knowledge_base/retrieval/hit_stopwords.txt +767 -0
  86. astrbot/core/knowledge_base/retrieval/manager.py +276 -0
  87. astrbot/core/knowledge_base/retrieval/rank_fusion.py +142 -0
  88. astrbot/core/knowledge_base/retrieval/sparse_retriever.py +136 -0
  89. astrbot/core/log.py +21 -15
  90. astrbot/core/message/components.py +413 -287
  91. astrbot/core/message/message_event_result.py +35 -24
  92. astrbot/core/persona_mgr.py +192 -0
  93. astrbot/core/pipeline/__init__.py +14 -14
  94. astrbot/core/pipeline/content_safety_check/stage.py +13 -9
  95. astrbot/core/pipeline/content_safety_check/strategies/__init__.py +1 -2
  96. astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +13 -14
  97. astrbot/core/pipeline/content_safety_check/strategies/keywords.py +2 -1
  98. astrbot/core/pipeline/content_safety_check/strategies/strategy.py +6 -6
  99. astrbot/core/pipeline/context.py +7 -1
  100. astrbot/core/pipeline/context_utils.py +107 -0
  101. astrbot/core/pipeline/preprocess_stage/stage.py +63 -36
  102. astrbot/core/pipeline/process_stage/method/agent_request.py +48 -0
  103. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +464 -0
  104. astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +202 -0
  105. astrbot/core/pipeline/process_stage/method/star_request.py +26 -32
  106. astrbot/core/pipeline/process_stage/stage.py +21 -15
  107. astrbot/core/pipeline/process_stage/utils.py +125 -0
  108. astrbot/core/pipeline/rate_limit_check/stage.py +34 -36
  109. astrbot/core/pipeline/respond/stage.py +142 -101
  110. astrbot/core/pipeline/result_decorate/stage.py +124 -57
  111. astrbot/core/pipeline/scheduler.py +21 -16
  112. astrbot/core/pipeline/session_status_check/stage.py +37 -0
  113. astrbot/core/pipeline/stage.py +11 -76
  114. astrbot/core/pipeline/waking_check/stage.py +69 -33
  115. astrbot/core/pipeline/whitelist_check/stage.py +10 -7
  116. astrbot/core/platform/__init__.py +6 -6
  117. astrbot/core/platform/astr_message_event.py +107 -129
  118. astrbot/core/platform/astrbot_message.py +32 -12
  119. astrbot/core/platform/manager.py +62 -18
  120. astrbot/core/platform/message_session.py +30 -0
  121. astrbot/core/platform/platform.py +16 -24
  122. astrbot/core/platform/platform_metadata.py +9 -4
  123. astrbot/core/platform/register.py +12 -7
  124. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +136 -60
  125. astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +126 -46
  126. astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +63 -31
  127. astrbot/core/platform/sources/dingtalk/dingtalk_event.py +30 -26
  128. astrbot/core/platform/sources/discord/client.py +129 -0
  129. astrbot/core/platform/sources/discord/components.py +139 -0
  130. astrbot/core/platform/sources/discord/discord_platform_adapter.py +473 -0
  131. astrbot/core/platform/sources/discord/discord_platform_event.py +313 -0
  132. astrbot/core/platform/sources/lark/lark_adapter.py +27 -18
  133. astrbot/core/platform/sources/lark/lark_event.py +39 -13
  134. astrbot/core/platform/sources/misskey/misskey_adapter.py +770 -0
  135. astrbot/core/platform/sources/misskey/misskey_api.py +964 -0
  136. astrbot/core/platform/sources/misskey/misskey_event.py +163 -0
  137. astrbot/core/platform/sources/misskey/misskey_utils.py +550 -0
  138. astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +149 -33
  139. astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +41 -26
  140. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +36 -17
  141. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_event.py +3 -1
  142. astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +14 -8
  143. astrbot/core/platform/sources/satori/satori_adapter.py +792 -0
  144. astrbot/core/platform/sources/satori/satori_event.py +432 -0
  145. astrbot/core/platform/sources/slack/client.py +164 -0
  146. astrbot/core/platform/sources/slack/slack_adapter.py +416 -0
  147. astrbot/core/platform/sources/slack/slack_event.py +253 -0
  148. astrbot/core/platform/sources/telegram/tg_adapter.py +100 -43
  149. astrbot/core/platform/sources/telegram/tg_event.py +136 -36
  150. astrbot/core/platform/sources/webchat/webchat_adapter.py +72 -22
  151. astrbot/core/platform/sources/webchat/webchat_event.py +46 -22
  152. astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +35 -0
  153. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_adapter.py +926 -0
  154. astrbot/core/platform/sources/wechatpadpro/wechatpadpro_message_event.py +178 -0
  155. astrbot/core/platform/sources/wechatpadpro/xml_data_parser.py +159 -0
  156. astrbot/core/platform/sources/wecom/wecom_adapter.py +169 -27
  157. astrbot/core/platform/sources/wecom/wecom_event.py +162 -77
  158. astrbot/core/platform/sources/wecom/wecom_kf.py +279 -0
  159. astrbot/core/platform/sources/wecom/wecom_kf_message.py +196 -0
  160. astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +297 -0
  161. astrbot/core/platform/sources/wecom_ai_bot/__init__.py +15 -0
  162. astrbot/core/platform/sources/wecom_ai_bot/ierror.py +19 -0
  163. astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +472 -0
  164. astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +417 -0
  165. astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +152 -0
  166. astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +153 -0
  167. astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +168 -0
  168. astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +209 -0
  169. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +306 -0
  170. astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +186 -0
  171. astrbot/core/platform_message_history_mgr.py +49 -0
  172. astrbot/core/provider/__init__.py +2 -3
  173. astrbot/core/provider/entites.py +8 -8
  174. astrbot/core/provider/entities.py +154 -98
  175. astrbot/core/provider/func_tool_manager.py +446 -458
  176. astrbot/core/provider/manager.py +345 -207
  177. astrbot/core/provider/provider.py +188 -73
  178. astrbot/core/provider/register.py +9 -7
  179. astrbot/core/provider/sources/anthropic_source.py +295 -115
  180. astrbot/core/provider/sources/azure_tts_source.py +224 -0
  181. astrbot/core/provider/sources/bailian_rerank_source.py +236 -0
  182. astrbot/core/provider/sources/dashscope_tts.py +138 -14
  183. astrbot/core/provider/sources/edge_tts_source.py +24 -19
  184. astrbot/core/provider/sources/fishaudio_tts_api_source.py +58 -13
  185. astrbot/core/provider/sources/gemini_embedding_source.py +61 -0
  186. astrbot/core/provider/sources/gemini_source.py +310 -132
  187. astrbot/core/provider/sources/gemini_tts_source.py +81 -0
  188. astrbot/core/provider/sources/groq_source.py +15 -0
  189. astrbot/core/provider/sources/gsv_selfhosted_source.py +151 -0
  190. astrbot/core/provider/sources/gsvi_tts_source.py +14 -7
  191. astrbot/core/provider/sources/minimax_tts_api_source.py +159 -0
  192. astrbot/core/provider/sources/openai_embedding_source.py +40 -0
  193. astrbot/core/provider/sources/openai_source.py +241 -145
  194. astrbot/core/provider/sources/openai_tts_api_source.py +18 -7
  195. astrbot/core/provider/sources/sensevoice_selfhosted_source.py +13 -11
  196. astrbot/core/provider/sources/vllm_rerank_source.py +71 -0
  197. astrbot/core/provider/sources/volcengine_tts.py +115 -0
  198. astrbot/core/provider/sources/whisper_api_source.py +18 -13
  199. astrbot/core/provider/sources/whisper_selfhosted_source.py +19 -12
  200. astrbot/core/provider/sources/xinference_rerank_source.py +116 -0
  201. astrbot/core/provider/sources/xinference_stt_provider.py +197 -0
  202. astrbot/core/provider/sources/zhipu_source.py +6 -73
  203. astrbot/core/star/__init__.py +43 -11
  204. astrbot/core/star/config.py +17 -18
  205. astrbot/core/star/context.py +362 -138
  206. astrbot/core/star/filter/__init__.py +4 -3
  207. astrbot/core/star/filter/command.py +111 -35
  208. astrbot/core/star/filter/command_group.py +46 -34
  209. astrbot/core/star/filter/custom_filter.py +6 -5
  210. astrbot/core/star/filter/event_message_type.py +4 -2
  211. astrbot/core/star/filter/permission.py +4 -2
  212. astrbot/core/star/filter/platform_adapter_type.py +45 -12
  213. astrbot/core/star/filter/regex.py +4 -2
  214. astrbot/core/star/register/__init__.py +19 -15
  215. astrbot/core/star/register/star.py +41 -13
  216. astrbot/core/star/register/star_handler.py +236 -86
  217. astrbot/core/star/session_llm_manager.py +280 -0
  218. astrbot/core/star/session_plugin_manager.py +170 -0
  219. astrbot/core/star/star.py +36 -43
  220. astrbot/core/star/star_handler.py +47 -85
  221. astrbot/core/star/star_manager.py +442 -260
  222. astrbot/core/star/star_tools.py +167 -45
  223. astrbot/core/star/updator.py +17 -20
  224. astrbot/core/umop_config_router.py +106 -0
  225. astrbot/core/updator.py +38 -13
  226. astrbot/core/utils/astrbot_path.py +39 -0
  227. astrbot/core/utils/command_parser.py +1 -1
  228. astrbot/core/utils/io.py +119 -60
  229. astrbot/core/utils/log_pipe.py +1 -1
  230. astrbot/core/utils/metrics.py +11 -10
  231. astrbot/core/utils/migra_helper.py +73 -0
  232. astrbot/core/utils/path_util.py +63 -62
  233. astrbot/core/utils/pip_installer.py +37 -15
  234. astrbot/core/utils/session_lock.py +29 -0
  235. astrbot/core/utils/session_waiter.py +19 -20
  236. astrbot/core/utils/shared_preferences.py +174 -34
  237. astrbot/core/utils/t2i/__init__.py +4 -1
  238. astrbot/core/utils/t2i/local_strategy.py +386 -238
  239. astrbot/core/utils/t2i/network_strategy.py +109 -49
  240. astrbot/core/utils/t2i/renderer.py +29 -14
  241. astrbot/core/utils/t2i/template/astrbot_powershell.html +184 -0
  242. astrbot/core/utils/t2i/template_manager.py +111 -0
  243. astrbot/core/utils/tencent_record_helper.py +115 -1
  244. astrbot/core/utils/version_comparator.py +10 -13
  245. astrbot/core/zip_updator.py +112 -65
  246. astrbot/dashboard/routes/__init__.py +20 -13
  247. astrbot/dashboard/routes/auth.py +20 -9
  248. astrbot/dashboard/routes/chat.py +297 -141
  249. astrbot/dashboard/routes/config.py +652 -55
  250. astrbot/dashboard/routes/conversation.py +107 -37
  251. astrbot/dashboard/routes/file.py +26 -0
  252. astrbot/dashboard/routes/knowledge_base.py +1244 -0
  253. astrbot/dashboard/routes/log.py +27 -2
  254. astrbot/dashboard/routes/persona.py +202 -0
  255. astrbot/dashboard/routes/plugin.py +197 -139
  256. astrbot/dashboard/routes/route.py +27 -7
  257. astrbot/dashboard/routes/session_management.py +354 -0
  258. astrbot/dashboard/routes/stat.py +85 -18
  259. astrbot/dashboard/routes/static_file.py +5 -2
  260. astrbot/dashboard/routes/t2i.py +233 -0
  261. astrbot/dashboard/routes/tools.py +184 -120
  262. astrbot/dashboard/routes/update.py +59 -36
  263. astrbot/dashboard/server.py +96 -36
  264. astrbot/dashboard/utils.py +165 -0
  265. astrbot-4.7.0.dist-info/METADATA +294 -0
  266. astrbot-4.7.0.dist-info/RECORD +274 -0
  267. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/WHEEL +1 -1
  268. astrbot/core/db/plugin/sqlite_impl.py +0 -112
  269. astrbot/core/db/sqlite_init.sql +0 -50
  270. astrbot/core/pipeline/platform_compatibility/stage.py +0 -56
  271. astrbot/core/pipeline/process_stage/method/llm_request.py +0 -606
  272. astrbot/core/platform/sources/gewechat/client.py +0 -806
  273. astrbot/core/platform/sources/gewechat/downloader.py +0 -55
  274. astrbot/core/platform/sources/gewechat/gewechat_event.py +0 -255
  275. astrbot/core/platform/sources/gewechat/gewechat_platform_adapter.py +0 -103
  276. astrbot/core/platform/sources/gewechat/xml_data_parser.py +0 -110
  277. astrbot/core/provider/sources/dashscope_source.py +0 -203
  278. astrbot/core/provider/sources/dify_source.py +0 -281
  279. astrbot/core/provider/sources/llmtuner_source.py +0 -132
  280. astrbot/core/rag/embedding/openai_source.py +0 -20
  281. astrbot/core/rag/knowledge_db_mgr.py +0 -94
  282. astrbot/core/rag/store/__init__.py +0 -9
  283. astrbot/core/rag/store/chroma_db.py +0 -42
  284. astrbot/core/utils/dify_api_client.py +0 -152
  285. astrbot-3.5.6.dist-info/METADATA +0 -249
  286. astrbot-3.5.6.dist-info/RECORD +0 -158
  287. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/entry_points.txt +0 -0
  288. {astrbot-3.5.6.dist-info → astrbot-4.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import os
2
3
  import aiohttp
3
4
  import ssl
4
5
  import certifi
@@ -10,38 +11,40 @@ from astrbot.core.config import VERSION
10
11
  from . import RenderStrategy
11
12
  from PIL import ImageFont, Image, ImageDraw
12
13
  from astrbot.core.utils.io import save_temp_img
14
+ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
13
15
 
14
16
 
15
17
  class FontManager:
16
18
  """字体管理类,负责加载和缓存字体"""
17
-
19
+
18
20
  _font_cache = {}
19
-
21
+
20
22
  @classmethod
21
23
  def get_font(cls, size: int) -> ImageFont.FreeTypeFont:
22
24
  """获取指定大小的字体,优先从缓存获取"""
23
25
  if size in cls._font_cache:
24
26
  return cls._font_cache[size]
25
-
27
+
26
28
  # 首先尝试加载自定义字体
27
29
  try:
28
- font = ImageFont.truetype("data/font.ttf", size)
30
+ font_path = os.path.join(get_astrbot_data_path(), "font.ttf")
31
+ font = ImageFont.truetype(font_path, size)
29
32
  cls._font_cache[size] = font
30
33
  return font
31
34
  except Exception:
32
35
  pass
33
-
36
+
34
37
  # 跨平台常见字体列表
35
38
  fonts = [
36
- "msyh.ttc", # Windows
39
+ "msyh.ttc", # Windows
37
40
  "NotoSansCJK-Regular.ttc", # Linux
38
- "msyhbd.ttc", # Windows
39
- "PingFang.ttc", # macOS
40
- "Heiti.ttc", # macOS
41
- "Arial.ttf", # 通用
42
- "DejaVuSans.ttf", # Linux
41
+ "msyhbd.ttc", # Windows
42
+ "PingFang.ttc", # macOS
43
+ "Heiti.ttc", # macOS
44
+ "Arial.ttf", # 通用
45
+ "DejaVuSans.ttf", # Linux
43
46
  ]
44
-
47
+
45
48
  for font_name in fonts:
46
49
  try:
47
50
  font = ImageFont.truetype(font_name, size)
@@ -49,7 +52,7 @@ class FontManager:
49
52
  return font
50
53
  except Exception:
51
54
  continue
52
-
55
+
53
56
  # 如果所有字体都失败,使用默认字体
54
57
  try:
55
58
  default_font = ImageFont.load_default()
@@ -61,24 +64,30 @@ class FontManager:
61
64
 
62
65
  class TextMeasurer:
63
66
  """测量文本尺寸的工具类"""
64
-
67
+
65
68
  @staticmethod
66
69
  def get_text_size(text: str, font: ImageFont.FreeTypeFont) -> Tuple[int, int]:
67
70
  """获取文本的尺寸"""
68
71
  try:
69
72
  # PIL 9.0.0 以上版本
70
- return font.getbbox(text)[2:] if hasattr(font, 'getbbox') else font.getsize(text)
73
+ return (
74
+ font.getbbox(text)[2:]
75
+ if hasattr(font, "getbbox")
76
+ else font.getsize(text)
77
+ )
71
78
  except Exception:
72
79
  # 兼容旧版本
73
80
  return font.getsize(text)
74
81
 
75
82
  @staticmethod
76
- def split_text_to_fit_width(text: str, font: ImageFont.FreeTypeFont, max_width: int) -> List[str]:
83
+ def split_text_to_fit_width(
84
+ text: str, font: ImageFont.FreeTypeFont, max_width: int
85
+ ) -> List[str]:
77
86
  """将文本拆分为多行,确保每行不超过指定宽度"""
78
87
  lines = []
79
88
  if not text:
80
89
  return lines
81
-
90
+
82
91
  remaining_text = text
83
92
  while remaining_text:
84
93
  # 如果文本宽度小于最大宽度,直接添加
@@ -86,7 +95,7 @@ class TextMeasurer:
86
95
  if text_width <= max_width:
87
96
  lines.append(remaining_text)
88
97
  break
89
-
98
+
90
99
  # 尝试逐字计算能放入当前行的最多字符
91
100
  for i in range(len(remaining_text), 0, -1):
92
101
  width = TextMeasurer.get_text_size(remaining_text[:i], font)[0]
@@ -98,69 +107,99 @@ class TextMeasurer:
98
107
  # 如果单个字符都放不下,强制放一个字符
99
108
  lines.append(remaining_text[0])
100
109
  remaining_text = remaining_text[1:]
101
-
110
+
102
111
  return lines
103
112
 
104
113
 
105
114
  class MarkdownElement(ABC):
106
115
  """Markdown元素的基类"""
107
-
116
+
108
117
  def __init__(self, content: str):
109
118
  self.content = content
110
-
119
+
111
120
  @abstractmethod
112
121
  def calculate_height(self, image_width: int, font_size: int) -> int:
113
122
  """计算元素的高度"""
114
123
  pass
115
-
124
+
116
125
  @abstractmethod
117
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
126
+ def render(
127
+ self,
128
+ image: Image.Image,
129
+ draw: ImageDraw.Draw,
130
+ x: int,
131
+ y: int,
132
+ image_width: int,
133
+ font_size: int,
134
+ ) -> int:
118
135
  """渲染元素到图像,返回新的y坐标"""
119
136
  pass
120
137
 
121
138
 
122
139
  class TextElement(MarkdownElement):
123
140
  """普通文本元素"""
124
-
141
+
125
142
  def calculate_height(self, image_width: int, font_size: int) -> int:
126
143
  if not self.content.strip():
127
144
  return 10 # 空行高度
128
-
145
+
129
146
  font = FontManager.get_font(font_size)
130
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
147
+ lines = TextMeasurer.split_text_to_fit_width(
148
+ self.content, font, image_width - 20
149
+ )
131
150
  return len(lines) * (font_size + 8)
132
-
133
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
151
+
152
+ def render(
153
+ self,
154
+ image: Image.Image,
155
+ draw: ImageDraw.Draw,
156
+ x: int,
157
+ y: int,
158
+ image_width: int,
159
+ font_size: int,
160
+ ) -> int:
134
161
  if not self.content.strip():
135
162
  return y + 10 # 空行
136
-
163
+
137
164
  font = FontManager.get_font(font_size)
138
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
139
-
165
+ lines = TextMeasurer.split_text_to_fit_width(
166
+ self.content, font, image_width - 20
167
+ )
168
+
140
169
  for line in lines:
141
170
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
142
171
  y += font_size + 8
143
-
172
+
144
173
  return y
145
174
 
146
175
 
147
176
  class BoldTextElement(MarkdownElement):
148
177
  """粗体文本元素"""
149
-
178
+
150
179
  def calculate_height(self, image_width: int, font_size: int) -> int:
151
180
  font = FontManager.get_font(font_size)
152
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
181
+ lines = TextMeasurer.split_text_to_fit_width(
182
+ self.content, font, image_width - 20
183
+ )
153
184
  return len(lines) * (font_size + 8)
154
-
155
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
185
+
186
+ def render(
187
+ self,
188
+ image: Image.Image,
189
+ draw: ImageDraw.Draw,
190
+ x: int,
191
+ y: int,
192
+ image_width: int,
193
+ font_size: int,
194
+ ) -> int:
156
195
  # 尝试使用粗体字体,如果没有则绘制两次模拟粗体效果
157
196
  try:
158
197
  bold_fonts = [
159
- "msyhbd.ttc", # 微软雅黑粗体 (Windows)
198
+ "msyhbd.ttc", # 微软雅黑粗体 (Windows)
160
199
  "Arial-Bold.ttf", # Arial粗体
161
200
  "DejaVuSans-Bold.ttf", # Linux粗体
162
201
  ]
163
-
202
+
164
203
  bold_font = None
165
204
  for font_name in bold_fonts:
166
205
  try:
@@ -168,48 +207,64 @@ class BoldTextElement(MarkdownElement):
168
207
  break
169
208
  except Exception:
170
209
  continue
171
-
210
+
172
211
  if bold_font:
173
- lines = TextMeasurer.split_text_to_fit_width(self.content, bold_font, image_width - 20)
212
+ lines = TextMeasurer.split_text_to_fit_width(
213
+ self.content, bold_font, image_width - 20
214
+ )
174
215
  for line in lines:
175
216
  draw.text((x, y), line, font=bold_font, fill=(0, 0, 0))
176
217
  y += font_size + 8
177
218
  else:
178
219
  # 如果没有粗体字体,则绘制两次文本轻微偏移以模拟粗体
179
220
  font = FontManager.get_font(font_size)
180
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
221
+ lines = TextMeasurer.split_text_to_fit_width(
222
+ self.content, font, image_width - 20
223
+ )
181
224
  for line in lines:
182
225
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
183
- draw.text((x+1, y), line, font=font, fill=(0, 0, 0))
226
+ draw.text((x + 1, y), line, font=font, fill=(0, 0, 0))
184
227
  y += font_size + 8
185
228
  except Exception:
186
229
  # 兜底方案:使用普通字体
187
230
  font = FontManager.get_font(font_size)
188
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
231
+ lines = TextMeasurer.split_text_to_fit_width(
232
+ self.content, font, image_width - 20
233
+ )
189
234
  for line in lines:
190
235
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
191
236
  y += font_size + 8
192
-
237
+
193
238
  return y
194
239
 
195
240
 
196
241
  class ItalicTextElement(MarkdownElement):
197
242
  """斜体文本元素"""
198
-
243
+
199
244
  def calculate_height(self, image_width: int, font_size: int) -> int:
200
245
  font = FontManager.get_font(font_size)
201
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
246
+ lines = TextMeasurer.split_text_to_fit_width(
247
+ self.content, font, image_width - 20
248
+ )
202
249
  return len(lines) * (font_size + 8)
203
-
204
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
250
+
251
+ def render(
252
+ self,
253
+ image: Image.Image,
254
+ draw: ImageDraw.Draw,
255
+ x: int,
256
+ y: int,
257
+ image_width: int,
258
+ font_size: int,
259
+ ) -> int:
205
260
  # 尝试使用斜体字体,如果没有则使用倾斜变换模拟斜体效果
206
261
  try:
207
262
  italic_fonts = [
208
- "msyhi.ttc", # 微软雅黑斜体 (Windows)
263
+ "msyhi.ttc", # 微软雅黑斜体 (Windows)
209
264
  "Arial-Italic.ttf", # Arial斜体
210
265
  "DejaVuSans-Oblique.ttf", # Linux斜体
211
266
  ]
212
-
267
+
213
268
  italic_font = None
214
269
  for font_name in italic_fonts:
215
270
  try:
@@ -217,312 +272,388 @@ class ItalicTextElement(MarkdownElement):
217
272
  break
218
273
  except Exception:
219
274
  continue
220
-
275
+
221
276
  if italic_font:
222
- lines = TextMeasurer.split_text_to_fit_width(self.content, italic_font, image_width - 20)
277
+ lines = TextMeasurer.split_text_to_fit_width(
278
+ self.content, italic_font, image_width - 20
279
+ )
223
280
  for line in lines:
224
281
  draw.text((x, y), line, font=italic_font, fill=(0, 0, 0))
225
282
  y += font_size + 8
226
283
  else:
227
284
  # 如果没有斜体字体,使用变换
228
285
  font = FontManager.get_font(font_size)
229
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
230
-
286
+ lines = TextMeasurer.split_text_to_fit_width(
287
+ self.content, font, image_width - 20
288
+ )
289
+
231
290
  for line in lines:
232
291
  # 先创建一个临时图像用于倾斜处理
233
292
  text_width, text_height = TextMeasurer.get_text_size(line, font)
234
- text_img = Image.new('RGBA', (text_width + 20, text_height + 10), (0, 0, 0, 0))
293
+ text_img = Image.new(
294
+ "RGBA", (text_width + 20, text_height + 10), (0, 0, 0, 0)
295
+ )
235
296
  text_draw = ImageDraw.Draw(text_img)
236
297
  text_draw.text((0, 0), line, font=font, fill=(0, 0, 0, 255))
237
-
298
+
238
299
  # 倾斜变换,使用仿射变换实现斜体效果
239
300
  # 变换矩阵: [1, 0.2, 0, 0, 1, 0]
240
301
  italic_img = text_img.transform(
241
- text_img.size,
242
- Image.AFFINE,
243
- (1, 0.2, 0, 0, 1, 0),
244
- Image.BICUBIC
302
+ text_img.size, Image.AFFINE, (1, 0.2, 0, 0, 1, 0), Image.BICUBIC
245
303
  )
246
-
304
+
247
305
  # 粘贴到原图像
248
306
  image.paste(italic_img, (x, y), italic_img)
249
307
  y += font_size + 8
250
308
  except Exception:
251
309
  # 兜底方案:使用普通字体
252
310
  font = FontManager.get_font(font_size)
253
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
311
+ lines = TextMeasurer.split_text_to_fit_width(
312
+ self.content, font, image_width - 20
313
+ )
254
314
  for line in lines:
255
315
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
256
316
  y += font_size + 8
257
-
317
+
258
318
  return y
259
319
 
260
320
 
261
321
  class UnderlineTextElement(MarkdownElement):
262
322
  """下划线文本元素"""
263
-
323
+
264
324
  def calculate_height(self, image_width: int, font_size: int) -> int:
265
325
  font = FontManager.get_font(font_size)
266
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
326
+ lines = TextMeasurer.split_text_to_fit_width(
327
+ self.content, font, image_width - 20
328
+ )
267
329
  return len(lines) * (font_size + 8)
268
-
269
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
330
+
331
+ def render(
332
+ self,
333
+ image: Image.Image,
334
+ draw: ImageDraw.Draw,
335
+ x: int,
336
+ y: int,
337
+ image_width: int,
338
+ font_size: int,
339
+ ) -> int:
270
340
  font = FontManager.get_font(font_size)
271
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
272
-
341
+ lines = TextMeasurer.split_text_to_fit_width(
342
+ self.content, font, image_width - 20
343
+ )
344
+
273
345
  for line in lines:
274
346
  # 绘制文本
275
347
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
276
-
348
+
277
349
  # 绘制下划线
278
350
  text_width, _ = TextMeasurer.get_text_size(line, font)
279
351
  underline_y = y + font_size + 2
280
- draw.line((x, underline_y, x + text_width, underline_y), fill=(0, 0, 0), width=1)
281
-
352
+ draw.line(
353
+ (x, underline_y, x + text_width, underline_y), fill=(0, 0, 0), width=1
354
+ )
355
+
282
356
  y += font_size + 8
283
-
357
+
284
358
  return y
285
359
 
286
360
 
287
361
  class StrikethroughTextElement(MarkdownElement):
288
362
  """删除线文本元素"""
289
-
363
+
290
364
  def calculate_height(self, image_width: int, font_size: int) -> int:
291
365
  font = FontManager.get_font(font_size)
292
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
366
+ lines = TextMeasurer.split_text_to_fit_width(
367
+ self.content, font, image_width - 20
368
+ )
293
369
  return len(lines) * (font_size + 8)
294
-
295
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
370
+
371
+ def render(
372
+ self,
373
+ image: Image.Image,
374
+ draw: ImageDraw.Draw,
375
+ x: int,
376
+ y: int,
377
+ image_width: int,
378
+ font_size: int,
379
+ ) -> int:
296
380
  font = FontManager.get_font(font_size)
297
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
298
-
381
+ lines = TextMeasurer.split_text_to_fit_width(
382
+ self.content, font, image_width - 20
383
+ )
384
+
299
385
  for line in lines:
300
386
  # 绘制文本
301
387
  draw.text((x, y), line, font=font, fill=(0, 0, 0))
302
-
388
+
303
389
  # 绘制删除线
304
390
  text_width, _ = TextMeasurer.get_text_size(line, font)
305
391
  strike_y = y + font_size // 2
306
392
  draw.line((x, strike_y, x + text_width, strike_y), fill=(0, 0, 0), width=1)
307
-
393
+
308
394
  y += font_size + 8
309
-
395
+
310
396
  return y
311
397
 
312
398
 
313
399
  class HeaderElement(MarkdownElement):
314
400
  """标题元素"""
315
-
401
+
316
402
  def __init__(self, content: str):
317
403
  # 去除开头的 # 并计算级别
318
404
  level = 0
319
405
  for char in content:
320
- if char == '#':
406
+ if char == "#":
321
407
  level += 1
322
408
  else:
323
409
  break
324
-
410
+
325
411
  super().__init__(content[level:].strip())
326
412
  self.level = min(level, 6) # h1-h6
327
-
413
+
328
414
  def calculate_height(self, image_width: int, font_size: int) -> int:
329
415
  header_font_size = 42 - (self.level - 1) * 4
330
416
  font = FontManager.get_font(header_font_size)
331
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 20)
417
+ lines = TextMeasurer.split_text_to_fit_width(
418
+ self.content, font, image_width - 20
419
+ )
332
420
  return len(lines) * header_font_size + 30 # 包含上下间距和分隔线
333
-
334
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
421
+
422
+ def render(
423
+ self,
424
+ image: Image.Image,
425
+ draw: ImageDraw.Draw,
426
+ x: int,
427
+ y: int,
428
+ image_width: int,
429
+ font_size: int,
430
+ ) -> int:
335
431
  header_font_size = 42 - (self.level - 1) * 4
336
432
  font = FontManager.get_font(header_font_size)
337
-
433
+
338
434
  y += 10 # 上间距
339
435
  draw.text((x, y), self.content, font=font, fill=(0, 0, 0))
340
-
436
+
341
437
  # 添加分隔线
342
438
  y += header_font_size + 8
343
- draw.line(
344
- (x, y, image_width - 10, y),
345
- fill=(230, 230, 230),
346
- width=3
347
- )
348
-
439
+ draw.line((x, y, image_width - 10, y), fill=(230, 230, 230), width=3)
440
+
349
441
  return y + 10 # 返回包含下间距的新y坐标
350
442
 
351
443
 
352
444
  class QuoteElement(MarkdownElement):
353
445
  """引用元素"""
354
-
446
+
355
447
  def __init__(self, content: str):
356
448
  # 去除开头的 >
357
449
  super().__init__(content[1:].strip())
358
-
450
+
359
451
  def calculate_height(self, image_width: int, font_size: int) -> int:
360
452
  font = FontManager.get_font(font_size)
361
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 30) # 左边留出引用线的空间
453
+ lines = TextMeasurer.split_text_to_fit_width(
454
+ self.content, font, image_width - 30
455
+ ) # 左边留出引用线的空间
362
456
  return len(lines) * (font_size + 6) + 12 # 包含上下间距
363
-
364
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
457
+
458
+ def render(
459
+ self,
460
+ image: Image.Image,
461
+ draw: ImageDraw.Draw,
462
+ x: int,
463
+ y: int,
464
+ image_width: int,
465
+ font_size: int,
466
+ ) -> int:
365
467
  font = FontManager.get_font(font_size)
366
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 30)
367
-
468
+ lines = TextMeasurer.split_text_to_fit_width(
469
+ self.content, font, image_width - 30
470
+ )
471
+
368
472
  total_height = len(lines) * (font_size + 6)
369
-
473
+
370
474
  # 绘制引用线
371
475
  quote_line_x = x + 3
372
476
  draw.line(
373
477
  (quote_line_x, y + 6, quote_line_x, y + total_height + 6),
374
478
  fill=(180, 180, 180),
375
- width=5
479
+ width=5,
376
480
  )
377
-
481
+
378
482
  # 绘制文本
379
483
  text_x = x + 15
380
484
  text_y = y + 6
381
485
  for line in lines:
382
486
  draw.text((text_x, text_y), line, font=font, fill=(180, 180, 180))
383
487
  text_y += font_size + 6
384
-
488
+
385
489
  return y + total_height + 12
386
490
 
387
491
 
388
492
  class ListItemElement(MarkdownElement):
389
493
  """列表项元素"""
390
-
494
+
391
495
  def calculate_height(self, image_width: int, font_size: int) -> int:
392
496
  font = FontManager.get_font(font_size)
393
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 30) # 左边留出项目符号的空间
497
+ lines = TextMeasurer.split_text_to_fit_width(
498
+ self.content, font, image_width - 30
499
+ ) # 左边留出项目符号的空间
394
500
  return len(lines) * (font_size + 6) + 16 # 包含上下间距
395
-
396
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
501
+
502
+ def render(
503
+ self,
504
+ image: Image.Image,
505
+ draw: ImageDraw.Draw,
506
+ x: int,
507
+ y: int,
508
+ image_width: int,
509
+ font_size: int,
510
+ ) -> int:
397
511
  font = FontManager.get_font(font_size)
398
- lines = TextMeasurer.split_text_to_fit_width(self.content, font, image_width - 30)
399
-
512
+ lines = TextMeasurer.split_text_to_fit_width(
513
+ self.content, font, image_width - 30
514
+ )
515
+
400
516
  y += 8 # 上间距
401
-
517
+
402
518
  # 绘制项目符号
403
519
  bullet_x = x + 5
404
520
  draw.text((bullet_x, y), "•", font=font, fill=(0, 0, 0))
405
-
521
+
406
522
  # 绘制文本
407
523
  text_x = x + 25
408
524
  text_y = y
409
525
  for line in lines:
410
526
  draw.text((text_x, text_y), line, font=font, fill=(0, 0, 0))
411
527
  text_y += font_size + 6
412
-
528
+
413
529
  return text_y + 8 # 包含下间距
414
530
 
415
531
 
416
532
  class CodeBlockElement(MarkdownElement):
417
533
  """代码块元素"""
418
-
534
+
419
535
  def __init__(self, content: List[str]):
420
536
  super().__init__("\n".join(content))
421
-
537
+
422
538
  def calculate_height(self, image_width: int, font_size: int) -> int:
423
539
  if not self.content:
424
540
  return 40 # 空代码块的最小高度
425
-
541
+
426
542
  font = FontManager.get_font(font_size)
427
543
  lines = self.content.split("\n")
428
544
  wrapped_lines = []
429
-
545
+
430
546
  for line in lines:
431
547
  wrapped = TextMeasurer.split_text_to_fit_width(line, font, image_width - 40)
432
548
  wrapped_lines.extend(wrapped)
433
-
549
+
434
550
  return len(wrapped_lines) * (font_size + 4) + 40 # 包含内边距和上下间距
435
-
436
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
551
+
552
+ def render(
553
+ self,
554
+ image: Image.Image,
555
+ draw: ImageDraw.Draw,
556
+ x: int,
557
+ y: int,
558
+ image_width: int,
559
+ font_size: int,
560
+ ) -> int:
437
561
  font = FontManager.get_font(font_size)
438
562
  lines = self.content.split("\n")
439
563
  wrapped_lines = []
440
-
564
+
441
565
  for line in lines:
442
566
  wrapped = TextMeasurer.split_text_to_fit_width(line, font, image_width - 40)
443
567
  wrapped_lines.extend(wrapped)
444
-
568
+
445
569
  content_height = len(wrapped_lines) * (font_size + 4)
446
570
  total_height = content_height + 30 # 包含内边距
447
-
571
+
448
572
  # 绘制背景
449
573
  draw.rounded_rectangle(
450
574
  (x, y + 5, image_width - 10, y + total_height),
451
575
  radius=5,
452
576
  fill=(240, 240, 240),
453
- width=1
577
+ width=1,
454
578
  )
455
-
579
+
456
580
  # 绘制代码
457
581
  text_y = y + 15
458
582
  for line in wrapped_lines:
459
583
  draw.text((x + 15, text_y), line, font=font, fill=(0, 0, 0))
460
584
  text_y += font_size + 4
461
-
585
+
462
586
  return y + total_height + 10
463
587
 
464
588
 
465
589
  class InlineCodeElement(MarkdownElement):
466
590
  """行内代码元素"""
467
-
591
+
468
592
  def calculate_height(self, image_width: int, font_size: int) -> int:
469
593
  return font_size + 16 # 包含内边距和上下间距
470
-
471
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
594
+
595
+ def render(
596
+ self,
597
+ image: Image.Image,
598
+ draw: ImageDraw.Draw,
599
+ x: int,
600
+ y: int,
601
+ image_width: int,
602
+ font_size: int,
603
+ ) -> int:
472
604
  font = FontManager.get_font(font_size)
473
-
605
+
474
606
  # 计算文本大小
475
607
  text_width, _ = TextMeasurer.get_text_size(self.content, font)
476
608
  text_height = font_size
477
-
609
+
478
610
  # 绘制背景
479
611
  padding = 4
480
612
  draw.rounded_rectangle(
481
- (
482
- x,
483
- y + 4,
484
- x + text_width + padding * 2,
485
- y + text_height + padding * 2 + 4
486
- ),
613
+ (x, y + 4, x + text_width + padding * 2, y + text_height + padding * 2 + 4),
487
614
  radius=5,
488
615
  fill=(230, 230, 230),
489
- width=1
616
+ width=1,
490
617
  )
491
-
618
+
492
619
  # 绘制文本
493
- draw.text((x + padding, y + padding + 4), self.content, font=font, fill=(0, 0, 0))
494
-
620
+ draw.text(
621
+ (x + padding, y + padding + 4), self.content, font=font, fill=(0, 0, 0)
622
+ )
623
+
495
624
  return y + text_height + 16 # 返回新的y坐标
496
625
 
497
626
 
498
627
  class ImageElement(MarkdownElement):
499
628
  """图片元素"""
500
-
629
+
501
630
  def __init__(self, content: str, image_url: str):
502
631
  super().__init__(content)
503
632
  self.image_url = image_url
504
633
  self.image = None
505
-
634
+
506
635
  async def load_image(self):
507
636
  """加载图片"""
508
637
  try:
509
638
  ssl_context = ssl.create_default_context(cafile=certifi.where())
510
639
  connector = aiohttp.TCPConnector(ssl=ssl_context)
511
-
512
- async with aiohttp.ClientSession(trust_env=True, connector=connector) as session:
640
+
641
+ async with aiohttp.ClientSession(
642
+ trust_env=True, connector=connector
643
+ ) as session:
513
644
  async with session.get(self.image_url) as resp:
514
- if (resp.status == 200):
645
+ if resp.status == 200:
515
646
  image_data = await resp.read()
516
647
  self.image = Image.open(BytesIO(image_data))
517
648
  else:
518
649
  print(f"Failed to load image: HTTP {resp.status}")
519
650
  except Exception as e:
520
651
  print(f"Failed to load image: {e}")
521
-
652
+
522
653
  def calculate_height(self, image_width: int, font_size: int) -> int:
523
654
  if self.image is None:
524
655
  return font_size + 20 # 图片加载失败的默认高度
525
-
656
+
526
657
  # 计算调整大小后的图片高度
527
658
  max_width = image_width * 0.8
528
659
  if self.image.width > max_width:
@@ -530,52 +661,60 @@ class ImageElement(MarkdownElement):
530
661
  height = int(self.image.height * ratio)
531
662
  else:
532
663
  height = self.image.height
533
-
664
+
534
665
  return height + 30 # 包含上下间距
535
-
536
- def render(self, image: Image.Image, draw: ImageDraw.Draw, x: int, y: int, image_width: int, font_size: int) -> int:
666
+
667
+ def render(
668
+ self,
669
+ image: Image.Image,
670
+ draw: ImageDraw.Draw,
671
+ x: int,
672
+ y: int,
673
+ image_width: int,
674
+ font_size: int,
675
+ ) -> int:
537
676
  if self.image is None:
538
677
  # 图片加载失败
539
678
  font = FontManager.get_font(font_size)
540
679
  draw.text((x, y + 10), "[图片加载失败]", font=font, fill=(255, 0, 0))
541
680
  return y + font_size + 20
542
-
681
+
543
682
  # 调整图片大小
544
683
  max_width = image_width * 0.8
545
684
  pasted_image = self.image
546
-
685
+
547
686
  if pasted_image.width > max_width:
548
687
  ratio = max_width / pasted_image.width
549
688
  new_size = (int(max_width), int(pasted_image.height * ratio))
550
689
  pasted_image = pasted_image.resize(new_size, Image.LANCZOS)
551
-
690
+
552
691
  # 计算居中位置
553
692
  paste_x = x + (image_width - pasted_image.width) // 2 - 10
554
-
693
+
555
694
  # 粘贴图片
556
- if pasted_image.mode == 'RGBA':
695
+ if pasted_image.mode == "RGBA":
557
696
  # 处理透明图片
558
697
  image.paste(pasted_image, (paste_x, y + 15), pasted_image)
559
698
  else:
560
699
  image.paste(pasted_image, (paste_x, y + 15))
561
-
700
+
562
701
  return y + pasted_image.height + 30
563
702
 
564
703
 
565
704
  class MarkdownParser:
566
705
  """Markdown解析器,将文本解析为元素"""
567
-
706
+
568
707
  @staticmethod
569
708
  async def parse(text: str) -> List[MarkdownElement]:
570
709
  elements = []
571
- lines = text.split('\n')
572
-
710
+ lines = text.split("\n")
711
+
573
712
  i = 0
574
713
  while i < len(lines):
575
714
  line = lines[i].rstrip()
576
-
715
+
577
716
  # 图片检测
578
- image_match = re.search(r'!\s*\[(.*?)\]\s*\((.*?)\)', line)
717
+ image_match = re.search(r"!\s*\[(.*?)\]\s*\((.*?)\)", line)
579
718
  if image_match:
580
719
  image_url = image_match.group(2)
581
720
  element = ImageElement(line, image_url)
@@ -583,101 +722,108 @@ class MarkdownParser:
583
722
  elements.append(element)
584
723
  i += 1
585
724
  continue
586
-
725
+
587
726
  # 标题
588
- if line.startswith('#'):
727
+ if line.startswith("#"):
589
728
  elements.append(HeaderElement(line))
590
729
  i += 1
591
730
  continue
592
-
731
+
593
732
  # 引用
594
- if line.startswith('>'):
733
+ if line.startswith(">"):
595
734
  elements.append(QuoteElement(line))
596
735
  i += 1
597
736
  continue
598
-
737
+
599
738
  # 列表项
600
- if line.startswith('-') or line.startswith('*'):
739
+ if line.startswith("-") or line.startswith("*"):
601
740
  elements.append(ListItemElement(line[1:].strip()))
602
741
  i += 1
603
742
  continue
604
-
743
+
605
744
  # 代码块
606
- if line.startswith('```'):
745
+ if line.startswith("```"):
607
746
  code_lines = []
608
747
  i += 1 # 跳过开始标记行
609
-
610
- while i < len(lines) and not lines[i].startswith('```'):
748
+
749
+ while i < len(lines) and not lines[i].startswith("```"):
611
750
  code_lines.append(lines[i])
612
751
  i += 1
613
-
752
+
614
753
  i += 1 # 跳过结束标记行
615
754
  elements.append(CodeBlockElement(code_lines))
616
755
  continue
617
-
756
+
618
757
  # 检查行内样式(粗体、斜体、下划线、删除线、行内代码)
619
- if re.search(r'(\*\*.*?\*\*)|(\*.*?\*)|(__.*?__)|(_.*?_)|(~~.*?~~)|(`.*?`)', line):
758
+ if re.search(
759
+ r"(\*\*.*?\*\*)|(\*.*?\*)|(__.*?__)|(_.*?_)|(~~.*?~~)|(`.*?`)", line
760
+ ):
620
761
  # 分析行内样式:
621
762
  # - 粗体: **text** 或 __text__
622
763
  # - 斜体: *text* 或 _text_
623
764
  # - 删除线: ~~text~~
624
765
  # - 行内代码: `text`
625
-
766
+
626
767
  # 定义正则模式和对应的元素类型
627
768
  patterns = [
628
- (r'\*\*(.*?)\*\*', BoldTextElement), # **粗体**
629
- (r'__(.*?)__', BoldTextElement), # __粗体__
630
- (r'\*((?!\*\*).*?)\*', ItalicTextElement), # *斜体* (但不匹配 ** 开头)
631
- (r'_((?!__).*?)_', ItalicTextElement), # _斜体_ (但不匹配 __ 开头)
632
- (r'~~(.*?)~~', StrikethroughTextElement), # ~~删除线~~
633
- (r'__(.*?)__', UnderlineTextElement), # __下划线__
634
- (r'`(.*?)`', InlineCodeElement) # `行内代码`
769
+ (r"\*\*(.*?)\*\*", BoldTextElement), # **粗体**
770
+ (r"__(.*?)__", BoldTextElement), # __粗体__
771
+ (
772
+ r"\*((?!\*\*).*?)\*",
773
+ ItalicTextElement,
774
+ ), # *斜体* (但不匹配 ** 开头)
775
+ (r"_((?!__).*?)_", ItalicTextElement), # _斜体_ (但不匹配 __ 开头)
776
+ (r"~~(.*?)~~", StrikethroughTextElement), # ~~删除线~~
777
+ (r"__(.*?)__", UnderlineTextElement), # __下划线__
778
+ (r"`(.*?)`", InlineCodeElement), # `行内代码`
635
779
  ]
636
-
780
+
637
781
  # 创建标记位置列表
638
782
  markers = []
639
783
  for pattern, element_class in patterns:
640
784
  for match in re.finditer(pattern, line):
641
- markers.append({
642
- 'start': match.start(),
643
- 'end': match.end(),
644
- 'text': match.group(1), # 提取内容部分
645
- 'element_class': element_class
646
- })
647
-
785
+ markers.append(
786
+ {
787
+ "start": match.start(),
788
+ "end": match.end(),
789
+ "text": match.group(1), # 提取内容部分
790
+ "element_class": element_class,
791
+ }
792
+ )
793
+
648
794
  # 按开始位置排序
649
- markers.sort(key=lambda x: x['start'])
650
-
795
+ markers.sort(key=lambda x: x["start"])
796
+
651
797
  # 如果没有找到任何匹配,直接添加为普通文本
652
798
  if not markers:
653
799
  elements.append(TextElement(line))
654
800
  i += 1
655
801
  continue
656
-
802
+
657
803
  # 处理每个文本片段
658
804
  current_pos = 0
659
805
  for marker in markers:
660
806
  # 添加前面的普通文本
661
- if marker['start'] > current_pos:
662
- normal_text = line[current_pos:marker['start']]
807
+ if marker["start"] > current_pos:
808
+ normal_text = line[current_pos : marker["start"]]
663
809
  if normal_text:
664
810
  elements.append(TextElement(normal_text))
665
-
811
+
666
812
  # 添加特殊样式的文本
667
- elements.append(marker['element_class'](marker['text']))
668
- current_pos = marker['end']
669
-
813
+ elements.append(marker["element_class"](marker["text"]))
814
+ current_pos = marker["end"]
815
+
670
816
  # 添加最后一段普通文本
671
817
  if current_pos < len(line):
672
818
  elements.append(TextElement(line[current_pos:]))
673
-
819
+
674
820
  i += 1
675
821
  continue
676
-
822
+
677
823
  # 行内代码 (如果之前没匹配到混合样式)
678
- inline_code_matches = re.findall(r'`([^`]+)`', line)
824
+ inline_code_matches = re.findall(r"`([^`]+)`", line)
679
825
  if inline_code_matches:
680
- parts = re.split(r'`([^`]+)`', line)
826
+ parts = re.split(r"`([^`]+)`", line)
681
827
  for j, part in enumerate(parts):
682
828
  if j % 2 == 0: # 普通文本
683
829
  if part:
@@ -686,88 +832,90 @@ class MarkdownParser:
686
832
  elements.append(InlineCodeElement(part))
687
833
  i += 1
688
834
  continue
689
-
835
+
690
836
  # 普通文本
691
837
  elements.append(TextElement(line))
692
838
  i += 1
693
-
839
+
694
840
  return elements
695
841
 
696
842
 
697
843
  class MarkdownRenderer:
698
844
  """Markdown渲染器,将元素渲染为图像"""
699
-
700
- def __init__(self, font_size: int = 26, width: int = 800, bg_color: Tuple[int, int, int] = (255, 255, 255)):
845
+
846
+ def __init__(
847
+ self,
848
+ font_size: int = 26,
849
+ width: int = 800,
850
+ bg_color: Tuple[int, int, int] = (255, 255, 255),
851
+ ):
701
852
  self.font_size = font_size
702
853
  self.width = width
703
854
  self.bg_color = bg_color
704
-
855
+
705
856
  async def render(self, markdown_text: str) -> Image.Image:
706
857
  # 解析Markdown文本
707
858
  elements = await MarkdownParser.parse(markdown_text)
708
-
859
+
709
860
  # 计算总高度
710
861
  total_height = 20 # 初始边距
711
862
  for element in elements:
712
863
  total_height += element.calculate_height(self.width, self.font_size)
713
-
864
+
714
865
  # 为页脚添加额外空间
715
866
  footer_height = 40
716
867
  total_height += 20 + footer_height # 结束边距 + 页脚高度
717
-
868
+
718
869
  # 创建图像
719
- image = Image.new('RGB', (self.width, max(100, total_height)), self.bg_color)
870
+ image = Image.new("RGB", (self.width, max(100, total_height)), self.bg_color)
720
871
  draw = ImageDraw.Draw(image)
721
-
872
+
722
873
  # 渲染元素
723
874
  y = 10
724
875
  for element in elements:
725
876
  y = element.render(image, draw, 10, y, self.width, self.font_size)
726
-
877
+
727
878
  # 添加页脚
728
879
  # 克莱因蓝色,近似RGB为(0, 47, 167)
729
880
  klein_blue = (0, 47, 167)
730
881
  # 灰色
731
882
  grey_color = (130, 130, 130)
732
-
883
+
733
884
  # 绘制"Powered by AstrBot"文本
734
885
  footer_font_size = 20
735
886
  footer_font = FontManager.get_font(footer_font_size)
736
-
887
+
737
888
  # 获取"Powered by "和"AstrBot"的宽度以便居中
738
889
  powered_by_text = "Powered by "
739
890
  astrbot_text = f"AstrBot v{VERSION}"
740
-
891
+
741
892
  powered_by_width, _ = TextMeasurer.get_text_size(powered_by_text, footer_font)
742
893
  astrbot_width, _ = TextMeasurer.get_text_size(astrbot_text, footer_font)
743
-
894
+
744
895
  total_width = powered_by_width + astrbot_width
745
896
  x_start = (self.width - total_width) // 2
746
-
897
+
747
898
  footer_y = total_height - footer_height
748
-
899
+
749
900
  # 绘制"Powered by "(灰色)
750
901
  draw.text(
751
- (x_start, footer_y),
752
- powered_by_text,
753
- font=footer_font,
754
- fill=grey_color
902
+ (x_start, footer_y), powered_by_text, font=footer_font, fill=grey_color
755
903
  )
756
-
904
+
757
905
  # 绘制"AstrBot"(克莱因蓝)
758
906
  draw.text(
759
- (x_start + powered_by_width, footer_y),
760
- astrbot_text,
761
- font=footer_font,
762
- fill=klein_blue
907
+ (x_start + powered_by_width, footer_y),
908
+ astrbot_text,
909
+ font=footer_font,
910
+ fill=klein_blue,
763
911
  )
764
-
912
+
765
913
  return image
766
914
 
767
915
 
768
916
  class LocalRenderStrategy(RenderStrategy):
769
917
  """本地渲染策略实现"""
770
-
918
+
771
919
  async def render_custom_template(
772
920
  self, tmpl_str: str, tmpl_data: dict, return_url: bool = True
773
921
  ) -> str:
@@ -776,9 +924,9 @@ class LocalRenderStrategy(RenderStrategy):
776
924
  async def render(self, text: str, return_url: bool = False) -> str:
777
925
  # 创建渲染器
778
926
  renderer = MarkdownRenderer(font_size=26, width=800)
779
-
927
+
780
928
  # 渲染Markdown文本
781
929
  image = await renderer.render(text)
782
-
930
+
783
931
  # 保存图像并返回路径/URL
784
932
  return save_temp_img(image)