generated from A-kirami/nonebot-plugin-template
-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
1,721 additions
and
1,536 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import mimetypes | ||
import re | ||
from typing import TYPE_CHECKING | ||
|
||
import anyio | ||
from nonebot import logger, on_shell_command | ||
from nonebot.adapters.onebot.v11 import MessageSegment | ||
from nonebot.exception import ParserExit | ||
from nonebot.matcher import Matcher | ||
from nonebot.params import ShellCommandArgs | ||
from nonebot.rule import ArgumentParser, Namespace | ||
from nonebot_plugin_htmlrender import get_new_page | ||
from playwright.async_api import Request, Route | ||
from yarl import URL | ||
|
||
from ..help import FT_E, FT_S | ||
from ..resource import RES_BA_LOGO_JS_PATH, RES_PATH | ||
|
||
if TYPE_CHECKING: | ||
from . import HelpList | ||
|
||
|
||
help_list: "HelpList" = [ | ||
{ | ||
"func": "标题生成器", | ||
"trigger_method": "指令", | ||
"trigger_condition": "balogo", | ||
"brief_des": "生成 BA Logo 样式的图片", | ||
"detail_des": ( | ||
"生成 BA Logo 样式的图片\n" | ||
"感谢 nulla2011/Bluearchive-logo 项目以 MIT 协议开源了图片绘制代码\n" | ||
" \n" | ||
"可以用这些指令触发:\n" | ||
f"- {FT_S}balogo{FT_E}\n" | ||
f"- {FT_S}baLogo{FT_E}\n" | ||
f"- {FT_S}baLOGO{FT_E}\n" | ||
f"- {FT_S}ba标题{FT_E}\n" | ||
" \n" | ||
"指令示例:\n" | ||
f"- {FT_S}balogo Blue Archive{FT_E}\n" | ||
f'- {FT_S}balogo "我是" "秦始皇"{FT_E}(包含空格的文本请使用引号包裹)' | ||
), | ||
}, | ||
] | ||
|
||
|
||
RES_ROUTE_PREFIX = "https://bawiki.res/" | ||
|
||
|
||
async def res_router(route: Route, request: Request): | ||
res_name = URL(request.url).path[1:] | ||
logger.debug(f"Requested resource `{res_name}`") | ||
|
||
path = anyio.Path(RES_PATH / res_name) | ||
if not await path.exists(): | ||
logger.debug(f"Resource `{res_name}` not found") | ||
await route.abort() | ||
return | ||
|
||
mime = mimetypes.guess_type(path)[0] | ||
logger.debug(f"Resource `{res_name}` mimetype: {mime}, path: {path}") | ||
await route.fulfill(body=await path.read_bytes(), content_type=mime) | ||
|
||
|
||
async def get_logo(text_l: str, text_r: str) -> str: | ||
async with get_new_page() as page: | ||
await page.route(re.compile(f"^{RES_ROUTE_PREFIX}(.*)$"), res_router) | ||
await page.goto(f"{RES_ROUTE_PREFIX}web/empty.html") | ||
return await page.evaluate( | ||
RES_BA_LOGO_JS_PATH.read_text(encoding="u8"), | ||
[text_l, text_r], | ||
) | ||
|
||
|
||
parser = ArgumentParser("balogo", add_help=False) | ||
parser.add_argument("text_l") | ||
parser.add_argument("text_r") | ||
|
||
cmd_ba_logo = on_shell_command( | ||
"balogo", | ||
aliases={"baLogo", "baLOGO", "ba标题"}, | ||
parser=parser, | ||
) | ||
|
||
|
||
@cmd_ba_logo.handle() | ||
async def _(matcher: Matcher, err: ParserExit = ShellCommandArgs()): | ||
if err.message: | ||
await matcher.finish(f"参数解析失败:{err.message}") | ||
|
||
|
||
@cmd_ba_logo.handle() | ||
async def _(matcher: Matcher, arg: Namespace = ShellCommandArgs()): | ||
try: | ||
b64_url = await get_logo(arg.text_l, arg.text_r) | ||
except Exception: | ||
logger.exception("Error when generating image") | ||
await matcher.finish("遇到错误,请检查后台输出") | ||
await matcher.finish(MessageSegment.image(b64_url)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
// https://github.com/nulla2011/bluearchive-logo/blob/master/src/canvas.ts | ||
/** @param {[string, string]} */ | ||
async ([textL, textR]) => { | ||
const transparentBg = true; | ||
const fontSize = 84; | ||
const canvasHeight = 250; | ||
const canvasWidth = 50; | ||
const textBaseLine = 0.68; | ||
const horizontalTilt = -0.4; | ||
const paddingX = 10; | ||
const graphOffset = { X: -15, Y: 0 }; | ||
const hollowPath = [ | ||
[284, 136], | ||
[321, 153], | ||
[159, 410], | ||
[148, 403], | ||
]; | ||
|
||
const halo = document.createElement('img'); | ||
halo.id = 'halo'; | ||
halo.src = 'https://bawiki.res/halo.png'; | ||
|
||
const cross = document.createElement('img'); | ||
cross.id = 'cross'; | ||
cross.src = 'https://bawiki.res/cross.png'; | ||
|
||
// wait images loaded | ||
await Promise.all( | ||
[halo, cross].map( | ||
(img) => | ||
new Promise((resolve, reject) => { | ||
img.onload = resolve; | ||
img.onerror = reject; | ||
}) | ||
) | ||
); | ||
|
||
// create canvas | ||
const canvas = document.createElement('canvas'); | ||
canvas.width = canvasWidth; | ||
canvas.height = canvasHeight; | ||
const c = canvas.getContext('2d'); | ||
|
||
// load font | ||
const font = `900 ${fontSize}px 'Ro GSan Serif Std B', 'Glow Sans SC', sans-serif`; | ||
await document.fonts.load(font, `${textL}${textR}`); | ||
c.font = font; | ||
|
||
// extend canvas | ||
const textMetricsL = c.measureText(textL); | ||
const textMetricsR = c.measureText(textR); | ||
|
||
const textWidthL = | ||
textMetricsL.width - | ||
(textBaseLine * canvasHeight + textMetricsL.fontBoundingBoxDescent) * | ||
horizontalTilt; | ||
const textWidthR = | ||
textMetricsR.width + | ||
(textBaseLine * canvasHeight - textMetricsR.fontBoundingBoxAscent) * | ||
horizontalTilt; | ||
|
||
const canvasWidthL = | ||
textWidthL + paddingX > canvasWidth / 2 | ||
? textWidthL + paddingX | ||
: canvasWidth / 2; | ||
const canvasWidthR = | ||
textWidthR + paddingX > canvasWidth / 2 | ||
? textWidthR + paddingX | ||
: canvasWidth / 2; | ||
|
||
canvas.width = canvasWidthL + canvasWidthR; | ||
|
||
// clear canvas | ||
c.clearRect(0, 0, canvas.width, canvas.height); | ||
|
||
// background | ||
if (!transparentBg) { | ||
c.fillStyle = '#fff'; | ||
c.fillRect(0, 0, canvas.width, canvas.height); | ||
} | ||
|
||
// left blue text | ||
c.font = font; | ||
c.fillStyle = '#128AFA'; | ||
c.textAlign = 'end'; | ||
c.setTransform(1, 0, horizontalTilt, 1, 0, 0); | ||
c.fillText(textL, canvasWidthL, canvas.height * textBaseLine); | ||
c.resetTransform(); // restore don't work | ||
|
||
// halo | ||
c.drawImage( | ||
halo, | ||
canvasWidthL - canvas.height / 2 + graphOffset.X, | ||
graphOffset.Y, | ||
canvasHeight, | ||
canvasHeight | ||
); | ||
|
||
// right black text | ||
c.fillStyle = '#2B2B2B'; | ||
c.textAlign = 'start'; | ||
if (transparentBg) c.globalCompositeOperation = 'destination-out'; | ||
c.strokeStyle = 'white'; | ||
c.lineWidth = 12; | ||
c.setTransform(1, 0, horizontalTilt, 1, 0, 0); | ||
c.strokeText(textR, canvasWidthL, canvas.height * textBaseLine); | ||
|
||
c.globalCompositeOperation = 'source-over'; | ||
c.fillText(textR, canvasWidthL, canvas.height * textBaseLine); | ||
c.resetTransform(); | ||
|
||
// cross stroke | ||
const graph = { | ||
X: canvasWidthL - canvas.height / 2 + graphOffset.X, | ||
Y: graphOffset.Y, | ||
}; | ||
c.beginPath(); | ||
hollowPath.forEach(([x, y], i) => { | ||
const f = (i === 0 ? c.moveTo : c.lineTo).bind(c); | ||
f(graph.X + x / 2, graph.Y + y / 2); | ||
}); | ||
c.closePath(); | ||
|
||
if (transparentBg) c.globalCompositeOperation = 'destination-out'; | ||
c.fillStyle = 'white'; | ||
c.fill(); | ||
c.globalCompositeOperation = 'source-over'; | ||
|
||
// cross | ||
c.drawImage( | ||
cross, | ||
canvasWidthL - canvas.height / 2 + graphOffset.X, | ||
graphOffset.Y, | ||
canvasHeight, | ||
canvasHeight | ||
); | ||
|
||
// output | ||
/** @type {HTMLCanvasElement} */ | ||
let outputCanvas; | ||
if ( | ||
textWidthL + paddingX >= canvasWidth / 2 && | ||
textWidthR + paddingX >= canvasWidth / 2 | ||
) { | ||
outputCanvas = canvas; | ||
} else { | ||
outputCanvas = document.createElement('canvas'); | ||
outputCanvas.width = textWidthL + textWidthR + paddingX * 2; | ||
outputCanvas.height = canvas.height; | ||
|
||
const ctx = outputCanvas.getContext('2d'); | ||
ctx.drawImage( | ||
canvas, | ||
canvasWidth / 2 - textWidthL - paddingX, | ||
0, | ||
textWidthL + textWidthR + paddingX * 2, | ||
canvas.height, | ||
0, | ||
0, | ||
textWidthL + textWidthR + paddingX * 2, | ||
canvas.height | ||
); | ||
} | ||
|
||
const b64 = outputCanvas.toDataURL().replace(/^data:image\/png;base64,/, ''); | ||
return `base64://${b64}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
</head> | ||
<body></body> | ||
</html> |
Oops, something went wrong.