🐛 [plugin]: 修复htmlredner造成的错误

This commit is contained in:
snowykami 2024-09-01 11:21:45 +08:00
parent 34a6261f27
commit 89cb75f105
13 changed files with 15 additions and 641 deletions

0
py.typed Normal file
View File

View File

@ -3,7 +3,7 @@ aiofiles>=23.2.1
colored>=2.2.4 colored>=2.2.4
GitPython>=3.1.43 GitPython>=3.1.43
httpx>=0.27.0 httpx>=0.27.0
liteyukibot-plugin-htmlrender>=0.1.0 nonebot-plugin-htmlrender>=0.1.0
nonebot2[fastapi,httpx,websockets]>=2.3.3 nonebot2[fastapi,httpx,websockets]>=2.3.3
nonebot-adapter-onebot>=2.4.3 nonebot-adapter-onebot>=2.4.3
nonebot-plugin-alconna>=0.46.3 nonebot-plugin-alconna>=0.46.3

View File

@ -18,11 +18,11 @@ from src.utils.base.config import get_config
from src.utils.base.data_manager import TempConfig, common_db from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import get_user_lang from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.htmlrender import md_to_pic
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
from .api import update_liteyuki from .api import update_liteyuki # type: ignore
from ..utils.base import reload from ..utils.base import reload # type: ignore
from ..utils.base.ly_function import get_function from ..utils.base.ly_function import get_function # type: ignore
from ..utils.message.html_tool import md_to_pic
require("nonebot_plugin_alconna") require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler") require("nonebot_plugin_apscheduler")
@ -42,8 +42,8 @@ driver = get_driver()
).handle() ).handle()
# Satori OK # Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma): async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
if result.main_args.get("text"): if text := result.main_args.get("text"):
await matcher.finish(Message(unescape(result.main_args.get("text")))) await matcher.finish(Message(unescape(text)))
else: else:
await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}") await matcher.finish(f"Hello, Liteyuki!\nBot {bot.self_id}")

View File

@ -22,7 +22,7 @@ from src.utils.base.permission import GROUP_ADMIN, GROUP_OWNER
from src.utils.message.tools import clamp from src.utils.message.tools import clamp
from src.utils.message.message import MarkdownMessage as md from src.utils.message.message import MarkdownMessage as md
from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md from src.utils.message.markdown import MarkdownComponent as mdc, compile_md, escape_md
from src.utils.htmlrender import md_to_pic from src.utils.message.html_tool import md_to_pic
from .common import * from .common import *

View File

@ -8,7 +8,7 @@ from src.utils.base.data_manager import User, user_db, group_db
from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang from src.utils.base.language import Language, change_user_lang, get_all_lang, get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md from src.utils.message.message import MarkdownMessage as md
from src.utils.htmlrender import md_to_pic from src.utils.message.html_tool import md_to_pic
from .const import representative_timezones_list from .const import representative_timezones_list
from src.utils import event as event_utils from src.utils import event as event_utils

View File

@ -1,155 +0,0 @@
**当前目录htmlrender内之全部文件除 Licenses\ 目录下的文件外) 须依照此文件所规定之特别声明进行授权**\
**该目录下所有文件(除 Licenses\ 目录下的文件外)所形成的整体(后文称“页面渲染组件”) 不可以从 轻雪(Liteyuki) 分离而单独运行**\
**且 页面渲染组件 本身为一不可割裂之整体独立存在**
**当文件所规定之本协议与 轻雪 项目之协议存在冲突,应遵循本文件中所述的协议进行行为**
**当前目录内 Licenses 目录下的文件,系 页面渲染组件 之制作过程中所使用、参考或改写的第三方项目所采用的协议。**\
**二次分发时应予以保留,并遵循相关协议的规定**\
**本文件所规定之协议与上述协议若存在冲突部分,应遵循本文件中所述的协议进行行为**
--------
版权所有 © 2024 金羿Eilles(EillesWan@outlook.com)
Copyright (R) 2024 EillesWan(EillesWan@outlook.com)
页面渲染组件 的 协议颁发者 是 金羿Eilles(EillesWan@outlook.com)
页面渲染组件 根据 第一版 汉钰律许可协议(“本协议”)授权。
任何人皆可从以下地址获得本协议副本:[轻雪仓库](https://github.com/LiteyukiStudio/LiteyukiBot/tree/main/src/utils/htmlrender/LICENSE.md)。
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。
详细的准许和限制条款请见原协议文本。
--------
以下为协议源文本
--------
# 汉钰律许可协议
**总第一版 · 二〇二四年七月七日编**
## 一、重要须知
1. 为保护采用本协议的作品在开源过程中,其著作权人所应有的权益,根据**中华人民共和国著作权法和相关法律法规**,制定本协议。
2. 本协议履行过程中,请注意本协议中**免除或限制**民事主体**责任或权利**的条款、法律适用和争议解决条款(尤其是加有特殊标记的条款),这些条款应在中国法律所允许的范围内最大程度地适用。
3. 若本协议所涉及的自然人**未满 18 周岁**,该自然人应在监护人的陪同下阅读本协议及有关本协议的条款内容,并在取得其监护人同意后开始或继续应用本协议所授权的行为。
4. 由于互联网服务、互联网内容的特殊性,若本协议以电子协议形式分发并签订,其依然有效。您一旦开始对本协议所授权之作品进行本协议所授权的行为,即视为您已经阅读、理解并同意并已经接受本协议的全部条款。
5. 本协议的订立、履行、解释及争议的解决均**适用中华人民共和国法律并排除其他一切冲突法的适用**。_本协议订立于许可证最初的颁发者的地址。若其为自然人则订立于该自然人户籍所在地若为法人或非法人组织则订立于其注册地_。本协议的订立各方应友好协商解决于协议所规定之行为的履行相关的争议如协商不成任何一方均可向合同签订地有管辖权的人民法院提起诉讼。
6. 本协议的原本仅为现代汉语,书写于简体中文。若存在其他语言的翻译或其他同等语言但非简体中文文本的版本,应当无法律效力。
## 二、术语定义
1. “**许可证**”、“**协议**”(后文称“本协议”)是指根据本文档中所列举的全部术语、定义、条款、限制等文本,是本合同的简称称谓。本合同全称是“汉钰律许可协议”。
2. “**协议颁发者**”(后文称“颁发者”)是将条款或协议应用于其拥有著作财产权的作品的民事主体,或由其指定从而拥有颁发者身份的民事主体。
3. “**源**”形式是指对包括但不限于 软件、硬件、文档、配置项 等种类的作品进行修改、编辑的首选形式;若不存在首选形式,则初次编辑该作品所需的形式即为源形式。
4. “**目标**”形式是指对源形式进行机械转换、翻译、打印、制造、加工等同类型活动后形成的结果形式,包括但不限于源代码编译后的目标软件、生成的文件、转换出的媒体、制造出的机械、打印出的实体文本、加工后的零件。
5. “**采用本协议的作品**”(后文称“此作品”)是指经颁发者授权而使用本协议进行授权的任何作品,该作品应在自然人可见处明确附加一个自然人可读的版权通知(可以参考文末附录中提供的示例);若在一个可分割的作品中,部分地采用本协议进行授权,则该部分应当视为一个独立的采用本协议的作品,该作品应当在自然人可见处明确附加一个自然人可读的范围限定和版权通知(同样可以参考文末附录中提供的示例)。
6. “**贡献**”是指对作品进行的,意在提交给此作品颁发者以让著作权人包含在其作品中的任何修订或补充,该修订或补充同样属于一种作品。依据此定义,“提交”一词表示经由此作品颁发者所指定的形式,将其所进行的修改发送给此作品颁发者。该形式应当包括在此作品颁发者指定的平台内发送易于编辑的修改信息、在此作品颁发者指定的电子邮箱中发送易于编辑的修改信息、在此作品颁发者指定的源码控制系统或发布跟踪系统上提交的易于编辑的修改信息,但由著作权人以明显标注或指定为“非贡献”的活动除外。颁发者自己对作品进行的修改同样视作对作品的贡献。
7. “**贡献者**”是指此作品颁发者接受的贡献的提交者,或包含在作品的贡献清单中的民事主体。贡献者在提交贡献并经此作品颁发者通过且该贡献已经被应用于此作品中后,该贡献者应当视为此作品的著作权人之一,但不应视为此作品非其贡献的部分的著作权人。一个作品的颁发者同样属于其贡献者。**请注意**,针对贡献者提交的贡献,该贡献者应被视为该贡献的协议颁发者,但不应视作本作品的颁发者。
8. “**用户**”、“**使用者**”是指行使本协议所授权之行为的民事主体。据此,贡献者亦属于用户。
9. “**商业性使用**”、“**商用**”是指任何以谋取利益为目的的使用,包括但不限于以贩卖、出租的形式对作品进行使用;但若将该牟利活动明确指示为“捐赠”,且在牟利者进行本协议所授权的活动时不以捐赠数额为标准,则此种的获取利益的“捐赠”行为不属于商业性使用。
## 三、权利授予
1. 任何由颁发者所进行的特殊声明、特别注意等此类内容,应当在法律效力上高于本协议的条款或声明;这些声明若与本协议冲突,本协议的该冲突部分无效;本协议与这些声明共同构成颁发者与用户之间的合同。
2. 此作品的贡献者享有其贡献的完整著作权。
3. 此作品的贡献者将自己的贡献的全部著作财产权,免费、公开、不可撤销、无限期、非专有地授予此作品的全部著作权人,并准许其在全世界范围内使用上述权利;若无明确的标识,贡献者允许此作品的颁发者对其贡献进行免费、公开、不可撤销、无限期、非专有、世界范围内的商业性使用。
4. 此作品的著作权人及贡献者授予用户**免费、公开、不可撤销、非专有、非商用**地以任意形式**复制、发行、展览、表演、放映、广播、信息网络传播、摄制、改编、翻译、汇编、二次授权**的权利,准许其在此作品颁发者所指定的区域与时间内行使上述权利;若此作品颁发者未特别指定的,则视作在全世界范围内无限期地授权;若此作品颁发者特别指定在特定情况下可以商用,则应当按照其所指定的条件进行商业性使用,商用的过程中,应当明确标识此作品的著作权人。
5. 一旦此作品有任意由非贡献形式而产生的更改,更改的部分将不视为此作品的一部分,除非该部分不可离开此作品单独存在;若该部分必须依赖此作品而不可与此作品分离从而单独存在,则更改后的作品不视作此作品,在这种情况下,除非此更改后的作品已获得此作品颁发者的特殊许可、或更改者即为此作品颁发者本人,否则对该作品进行的任何活动都应当遵守本协议。
6. 经贡献而产生的对此作品的更改,属于此作品的一部分;在此情况下,更改后的作品,依旧视作此作品。
7. 依据本款的第 4 条,若用户在本协议的授权下,将此作品授予他人进行任何形式的活动(即“二次授权”、“二次分发”),则应确保其使用的协议或授权内容,与本协议的条款不冲突;当存在与本协议条款的冲突时,则该冲突内容无效,被授权的第三方应依照本协议的条款进行活动;除非该用户获得了此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。
8. 依据本款的第 5 条,若由非贡献形式而产生更改的部分是可分割而不需依赖此作品即可单独存在的,若该部分明确注明不使用本协议进行授权或明确声明了其他授权条款,则该部分不视作采用本协议;但未更改的部分仍应视作原此作品的一部分,需要采用本协议进行授权,除非此更改后的作品已获得此作品颁发者的特殊许可、或更改者即为此作品颁发者本人。
9. 若此作品或所提交的贡献包含其著作权人的专利,则该专利所有人即此作品的著作权人应准许此作品全体著作权人**免费、公开、不可撤销、非专有、无版权费的专利许可**,以便贡献者对作品进行本协议所授权进行的活动。
10. 上述专利许可的授予,仅适用于在所提交的贡献中,可由专利所有者授予的,且在对此作品进行本协议所授权的活动中,必须使用的专利。
11. 如果用户对任何民事主体,因其在进行本协议所授权进行的活动中侵犯该用户的专利而提起诉讼,那么根据本协议授予该用户的所有关于此作品的任何其他专利许可将在提起上述诉讼之日起终止。
12. 如果本作品作为用户的其他作品的不可分割的一部分进行任何民事活动,本协议依旧对本作品(即该用户的其他作品的一部分)生效;若本作品完全融入该用户的其他作品之中而不可独立存在,则该用户需要保证其作品存在与本协议冲突的条款;除非该作品已获得此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。
## 四、使用条件
在对此作品进行本协议所授权的民事活动中,应当同时满足以下条款:
1. 用户必须为此作品的任何其他接收者提供本协议的副本,在不得已无法提供副本的情况下,也应明确指示其他接收者可查阅本协议的位置。
2. 用户必须在修改后的作品中附带明显的通知,声明用户已更改文件,并注明更改位置。
3. 若用户二次分发此作品,可以选择向此作品的接收者提供无偿或有偿的担保维修、支持服务或其他责任、义务。但是,该用户只可以其自己的名义提供上述内容,不得以任何其他贡献者的名义。且该用户必须明确表明任何此类责任或义务是由其个人独立提供,且其同意并应当承担赔偿此作品的全体贡献者因其个人承担上述责任义务而产生的任何赔偿责任。
4. 用户不得删除或更改此作品中包含的任何许可声明(包括版权声明,专利声明,免责声明,或赔偿责任限制),除非该更改是对已知事实错误的修补、或其已获得此作品颁发者的特殊许可、或更改者即为此作品颁发者本人。
5. 若此作品将权益的声明通知作为一部分,那么由用户分发的任何版本的作品中须至少在下列三处之一包含该声明通知的自然人可读副本:
- 该作品的权益声明通知中
- 在源形式的文件中(当且仅当该作品开放源代码)
- 在惯例中作为第三方通知出现之处(当且仅当该作品会产生画面,且该画面可被自然人详细观察)
该通知的内容仅供信息提供,不应对许可证进行任何文字上的修改。用户可在其分发的作品中,在不构成修改本协议的前提下,在作品自身的声明通知或属性描述后或作为附录添加。
6. 依据本款第3条若用户二次分发此作品时选择向作品的接收者提供收费的担保服务则必须明确告知该接收者本协议全部内容与此作品原出处并确保其知悉上述内容但若用户在二次分发此作品是不选择提供任何服务则该用户不允许向作品的接收者收取任何费用除非该用户获得了此作品颁发者的特殊许可、或该用户即为此作品颁发者本人。
## 五、提交贡献
除非贡献者明确声明,在本作品中由该贡献者向颁发者的提供的提交,必须符合本协议的条款,并与本协议的条款不存在冲突;除非此贡献中与本协议冲突的附加条款已获得颁发者的特殊许可、或贡献者即为此作品颁发者本人。
## 六、商标相关
本协议并未授予用户,将颁发者的商标、专属标记或特定产品名称,用于合理的或惯例性的描述或此类声明之外其他任何位置的权利。
## 七、免责声明
1. 若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,**不予提供任何形式的担保、任何明示、任何暗示或类似承诺**,此类包括但不限于担保此作品毫无缺陷、担保此作品适于贩卖、担保此作品适于特定目的、担保使用此作品绝不侵权。用户将自行承担因此作品的质量或性能问题而产生的全部风险。若此作品在任何方面欠妥,将由用户(而非任何贡献者、而非任何颁发者)承担所有必要的服务、维修或除错的任何成本。本免责声明本许可的重要组成部分。当且仅当遵守本免责声明时,本协议的其他条款中对本作品的使用授权方可生效。
2. 无论是因何种原因,如果不是在法律规定的特殊情况(如,确为贡献者的故意或重大过失)下或者经过了特殊准许,即使贡献者事先已知发生损害的可能,在使用本作品时,用户产生的任何直接、间接、特殊、偶然或必然造成的损失(包括但不限于商誉损失、工作延误、计算机系统故障等),**均不由任一贡献者承担**。
**以上是本许可协议的全部条款**
---
附录
**如何在自己的作品中应用 汉钰律许可协议**
若要在自己源形式的作品应用本协议,请在其中附加下面的通知模板,并将六角括号“〔〕”中的字段替换成自身的实际信息来替换(不包括括号本身)。这些文本必须以对应文件格式适当的注释句法包含在其中,可以是实体的纸质文档、也可以是网络公告或者计算机文件;或者脱离该源之外,另起一个新的文件,使之指向要应用本协议的那个作品。同时也建议将作品名或类别名以及目的说明之类的声明囊括在同一个可被打印的页面上作为版权通知的整体,这样更加容易的区分出第三方内容。
若需要在自己以目标形式存在的作品中应用本协议,同样需要附加下面的通知模板并更改六角括号中的字样。但是,这些文本可以是位于作品的标签上、位于作品的用户可见且能被自然人详细观察的画面之中、或者按照惯例中许可协议应该出现的位置;同时,这些文本的所处位置应当能够明确指示到本协议应用的那个作品。另外,建议将作品名或类别名以及目的说明之类的声明囊括在同一个可被打印的位置上作为版权通知的整体,这样更加容易的区分出第三方内容。
**通知模板**
```
版权所有 © 〔年份〕 〔著作权人〕
〔或者:版权所有 (C) 〔年份〕 〔著作权人〕〕
〔该作品〕根据 第一版 汉钰律许可协议(“本协议”)授权。
任何人皆可从以下地址获得本协议副本:〔本协议副本所在地址〕。
若非因法律要求或经过了特殊准许,此作品在根据本协议“原样”提供的基础上,不予提供任何形式的担保、任何明示、任何暗示或类似承诺。也就是说,用户将自行承担因此作品的质量或性能问题而产生的全部风险。
详细的准许和限制条款请见原协议文本。
```

View File

@ -1,31 +0,0 @@
LSO license
LiteyukiStudio Opensource license
---
版权所有 © 2024 Snowykami
---
免费向任何获得副本的人或组织授予以相同许可为基础的权利
包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售软件的副本
本软件及相关文档文件(以下简称"本软件")在相同方式许可为基础, 以开源的形式发布于互联网抑或其他媒体介质平台
任何人都有权利获取副本并以上述方式获取许可传播和/或使用
但获取副本时仍需注意:
- 上述版权声明和本许可声明应包含在本软件的副本中
- 使用本软件及其副本时仍需保持与原有形式相同
- 在使用时仍需将本软件的副本以相同许可公开表现:
- 不得未经原作者允许将本软件的副本以非原许可的形式对外盈利
---
该软件按"原样"之副本提供,不提供任何形式的任意保证,明示或暗示:
包括但不限于适销性保证, 适用于特定目的非侵权
在任何情况下, 作者或版权所有者对任何非因作者或版权所有者使用该软件造成的索赔、损害或其他责任, 无论是在合同诉讼、侵权行为还是其他诉讼中都不具有责任, 作者及其版权所有者有权利驳回使用者因个人原因造成的任何损失之赔付

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 kexue
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,3 +0,0 @@
# from .control import *
from .function import *

View File

@ -1,95 +0,0 @@
import platform
from contextlib import asynccontextmanager
from typing import Optional, AsyncIterator
import pyppeteer
import pyppeteer.browser
import pyppeteer.errors
import pyppeteer.page
from liteyuki.log import logger
# from liteyuki.plugin import PluginMetadata, PluginType
from src.utils.base.config import get_config
# __plugin_meta__ = PluginMetadata(
# name="页面渲染组件",
# description="提供跨平台的多用途页面渲染功能是nontbot-plugin-htmlrender的高级替代",
# type=PluginType.MODULE,
# author="金羿Eilles",
# extra={
# "license": "汉钰律许可协议 第一版",
# },
# )
_browser: Optional[pyppeteer.browser.Browser] = None
async def init(**kwargs) -> pyppeteer.browser.Browser:
global _browser
logger.info("正在初始化浏览器")
chromium_path = get_config("chromium_path")
if chromium_path:
try:
_browser = await pyppeteer.launch(executablePath=chromium_path, **kwargs)
except pyppeteer.errors.PyppeteerError as e:
logger.error(f"浏览器启动失败:{e}")
raise
logger.success("浏览器注册成功")
return _browser
else:
logger.error("请在配置文件中设置 chromium_path")
raise pyppeteer.errors.BrowserError(
"未配置浏览器地址若曾用过nonebot-plugin-htmlrender则可在 {} 处寻得一可用之chromium".format(
"%USERPROFILE%\\AppData\\Local\\ms-playwright"
if platform.system() == "Windows"
else (
"~/Library/Caches/ms-playwright"
if platform.system() == "Darwin"
else "~/.cache/ms-playwright"
)
)
)
async def get_browser(**kwargs) -> pyppeteer.browser.Browser:
return (
_browser
if _browser and _browser._connection._connected
else await init(**kwargs)
)
@asynccontextmanager
async def get_new_page(
viewport: Optional[dict] = None,
cookie: Optional[dict] = None,
useragent: Optional[str] = None,
) -> AsyncIterator[pyppeteer.page.Page]:
browser = await get_browser()
page = await browser.newPage()
# device_scale_factor=device_scale_factor, **kwargs
if viewport:
await page.setViewport(viewport)
if cookie:
await page.setCookie(cookie)
if useragent:
await page.setUserAgent(useragent)
try:
yield page
finally:
await page.close()
async def shutdown_browser():
global _browser
if _browser:
if _browser._connection._connected:
await _browser.close()
_browser = None

View File

@ -1,319 +0,0 @@
import os
import uuid
from typing import Any, Dict, Literal, Optional, Union
import jinja2
import aiofiles
import markdown
import pyppeteer.errors
# from pathlib import Path
from liteyuki.log import logger
from src.utils.base.resource import get_resource_path # , temp_extract_root
from .control import get_new_page
TEMPLATES_PATH = get_resource_path("templates", abs_path=True)
env = jinja2.Environment( # noqa: S701
extensions=["jinja2.ext.loopcontrols"],
loader=jinja2.FileSystemLoader(TEMPLATES_PATH),
enable_async=True,
)
async def read_any(path: str | os.PathLike[str], mode_: str = "r") -> str | bytes:
async with aiofiles.open(path, mode=mode_) as f: # type: ignore
return await f.read()
async def read_template(path: str) -> str:
return await read_any(TEMPLATES_PATH / path) # type: ignore
async def write_any(path: str | os.PathLike[str], content: str):
async with aiofiles.open(path, mode="w", encoding="utf-8") as f:
await f.write(content)
async def template_to_html(
template_path: str,
template_name: str,
**kwargs,
) -> str:
"""使用jinja2模板引擎通过html生成图片
Args:
template_path (str): 模板路径
template_name (str): 模板名
**kwargs: 模板内容
Returns:
str: html
"""
template_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_path),
enable_async=True,
)
template = template_env.get_template(template_name)
return await template.render_async(**kwargs)
async def html_to_pic(
html_path: str,
html: str = "",
wait: int = 0,
# template_path: str = "file://{}".format(os.getcwd()),
type_: Literal["jpeg", "png"] = "png", # noqa: A002
quality: Union[int, None] = None,
viewport: Optional[Dict[str, Any]] = None,
cookie: Optional[Dict[str, Any]] = None,
user_agent: Optional[str] = None,
device_scale_factor: float = 2,
) -> bytes:
"""html转图片
Args:
html (str): html文本若存在 JavaScript 脚本则无效
html_path (str, optional): HTML路径 "file:///path/to/template.html"
wait (int, optional): 等待时间单位毫秒默认为 0.
type (Literal["jpeg", "png"]): 图片类型默认 png
quality (int, optional): 图片质量 0-100 当为`png`时无效
viewport: (Dict[str, Any], optional): viewport 参数
cookie: (Dict[str, Any], optional): 页面 cookie
user_agent: (str, optional): 页面 UA
device_scale_factor: 缩放比例类型为float值越大越清晰(真正想让图片清晰更优先请调整此选项)
**kwargs: 传入 page 的参数
Returns:
bytes: 图片, 可直接发送
"""
# logger.debug(f"html:\n{html}")
if "file:" not in html_path:
raise Exception("html_path 应为 file:/// 协议之文件传递")
# open(
# filename := os.path.join(
# template_path,
# str(uuid.uuid4()) + ".html",
# ),
# "w",
# ).write(html)
logger.info("截入浏览器运作")
try:
async with get_new_page(viewport, cookie, user_agent) as page:
page.on("console", lambda msg: logger.debug(f"浏览器控制台: {msg.text}"))
await page.goto(html_path, waitUntil="networkidle0")
if html:
await page.setContent(
html,
)
await page.waitFor(wait)
logger.info("页面截屏")
return await page.screenshot(
fullPage=True,
type=type_,
quality=quality,
scale=device_scale_factor,
encoding="binary",
) # type: ignore
except pyppeteer.errors.PyppeteerError as e:
logger.error(f"浏览器页面获取出错: {e}")
return await read_any(TEMPLATES_PATH / "chromium_error.png", "rb") # type: ignore
async def template_to_pic(
template_path: str,
template_name: str,
templates: Dict[Any, Any],
pages: Optional[Dict[Any, Any]] = None,
wait: int = 0,
type_: Literal["jpeg", "png"] = "png", # noqa: A002
quality: Union[int, None] = None,
viewport: Optional[Dict[str, Any]] = None,
cookie: Optional[Dict[str, Any]] = None,
user_agent: Optional[str] = None,
device_scale_factor: float = 2,
) -> bytes:
"""使用jinja2模板引擎通过html生成图片
Args:
template_path (str): 模板路径
template_name (str): 模板名
templates (Dict[Any, Any]): 模板内参数 : {"name": "abc"}
pages (Optional[Dict[Any, Any]]): 网页参数已弃用
wait (int, optional): 网页载入等待时间. Defaults to 0.
type (Literal["jpeg", "png"]): 图片类型, 默认 png
quality (int, optional): 图片质量 0-100 当为`png`时无效
viewport: (Dict[str, Any], optional): viewport 参数
cookie: (Dict[str, Any], optional): 页面 cookie
user_agent: (str, optional): 页面 UA
device_scale_factor: 缩放比例,类型为float,值越大越清晰(真正想让图片清晰更优先请调整此选项)
Returns:
bytes: 图片 可直接发送
"""
if not viewport:
viewport = {"width": 500, "height": 10}
if pages and "viewport" in pages:
viewport.update(pages["viewport"])
if device_scale_factor:
viewport["deviceScaleFactor"] = device_scale_factor
template_env = jinja2.Environment( # noqa: S701
loader=jinja2.FileSystemLoader(template_path),
enable_async=True,
)
logger.info(
"template_name:{},template_path:{}".format(template_name, template_path)
)
template = template_env.get_template(template_name, template_path)
await write_any(
html_path_ := os.path.join(template_path, "{}.html".format(uuid.uuid4())),
await template.render_async(**templates),
)
picture_raw = await html_to_pic(
# html=html_content,
html_path="file://{}".format(html_path_),
wait=wait,
type_=type_,
quality=quality,
viewport=viewport,
cookie=cookie,
user_agent=user_agent,
)
os.remove(html_path_)
return picture_raw
async def text_to_pic(
text: str,
css_path: str = "",
width: int = 500,
type_: Literal["jpeg", "png"] = "png", # noqa: A002
quality: Union[int, None] = None,
device_scale_factor: float = 2,
) -> bytes:
"""多行文本转图片
Args:
text (str): 纯文本, 可多行
css_path (str, optional): css文件
width (int, optional): 图片宽度默认为 500
type (Literal["jpeg", "png"]): 图片类型, 默认 png
quality (int, optional): 图片质量 0-100 当为`png`时无效
device_scale_factor: 缩放比例,类型为float,值越大越清晰(真正想让图片清晰更优先请调整此选项)
Returns:
bytes: 图片, 可直接发送
"""
template = env.get_template("text.html")
return await html_to_pic(
html=await template.render_async(
text=text,
css=(
await read_any(css_path)
if css_path
else await read_template("text.css")
),
),
html_path=f"file://{css_path if css_path else TEMPLATES_PATH}",
viewport={
"width": width,
"height": 10,
"deviceScaleFactor": device_scale_factor,
},
type_=type_,
quality=quality,
)
async def md_to_pic(
md: str = "",
md_path: str = "",
css_path: str = "",
width: int = 500,
type_: Literal["jpeg", "png"] = "png", # noqa: A002
quality: Union[int, None] = None,
device_scale_factor: float = 2,
) -> bytes:
"""markdown 转 图片
Args:
md (str, optional): markdown 格式文本
md_path (str, optional): markdown 文件路径
css_path (str, optional): css文件路径. Defaults to None.
width (int, optional): 图片宽度默认为 500
type (Literal["jpeg", "png"]): 图片类型, 默认 png
quality (int, optional): 图片质量 0-100 当为`png`时无效
device_scale_factor: 缩放比例,类型为float,值越大越清晰(真正想让图片清晰更优先请调整此选项)
Returns:
bytes: 图片, 可直接发送
"""
template = env.get_template("markdown.html")
if not md:
if md_path:
md = await read_any(md_path) # type: ignore
else:
raise Exception("必须输入 md 或 md_path")
logger.debug(md)
md = markdown.markdown(
md,
extensions=[
"pymdownx.tasklist",
"tables",
"fenced_code",
"codehilite",
"mdx_math",
"pymdownx.tilde",
],
extension_configs={"mdx_math": {"enable_dollar_delimiter": True}},
)
logger.debug(md)
extra = ""
if "math/tex" in md:
katex_css = await read_template("katex/katex.min.b64_fonts.css")
katex_js = await read_template("katex/katex.min.js")
mathtex_js = await read_template("katex/mathtex-script-type.min.js")
extra = (
f'<style type="text/css">{katex_css}</style>'
f"<script defer>{katex_js}</script>"
f"<script defer>{mathtex_js}</script>"
)
if css_path:
css = await read_any(css_path)
else:
css = await read_template("github-markdown-light.css") + await read_template(
"pygments-default.css",
)
return await html_to_pic(
html=await template.render_async(md=md, css=css, extra=extra),
html_path=f"file://{css_path if css_path else TEMPLATES_PATH}",
viewport={
"width": width,
"height": 10,
"deviceScaleFactor": device_scale_factor,
},
type_=type_,
quality=quality,
)

View File

@ -1,14 +1,14 @@
import os.path import os
import aiofiles # type: ignore import aiofiles # type: ignore
import nonebot import nonebot
from nonebot import require from nonebot import require
require("nonebot_plugin_htmlrender") # require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import ( # type: ignore from nonebot_plugin_htmlrender import ( # type: ignore
template_to_html, template_to_html,
template_to_pic, template_to_pic,
md_to_pic
) # type: ignore ) # type: ignore
@ -85,3 +85,5 @@ async def template2image(
device_scale_factor=scale_factor device_scale_factor=scale_factor
### ###
) )

View File

@ -7,16 +7,12 @@ import aiofiles
import aiohttp import aiohttp
import nonebot import nonebot
from PIL import Image from PIL import Image
from nonebot import require
from nonebot.adapters import satori
from nonebot.adapters.onebot import v11 from nonebot.adapters.onebot import v11
from src.utils.htmlrender import md_to_pic from .html_tool import md_to_pic
from .. import load_from_yaml from .. import load_from_yaml
from ..base.ly_typing import T_Bot, T_Message, T_MessageEvent from ..base.ly_typing import T_Bot, T_Message, T_MessageEvent
config = load_from_yaml("config.yml") config = load_from_yaml("config.yml")