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''
f""
f""
)
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,
)