该代码是一个基于 Playwright 的异步工具函数,用于将 Mermaid 图表代码转换为多种格式的图像文件(如 PNG、SVG、PDF)。它通过启动一个无头浏览器,加载包含 Mermaid 渲染引擎的 HTML 页面,执行 Mermaid 代码生成 SVG,然后根据需求进行截图或导出为不同格式的文件。
graph TD
A[开始: 调用 mermaid_to_file] --> B[设置默认参数]
B --> C[启动 Playwright 无头浏览器]
C --> D[创建新页面并设置视口]
D --> E[加载本地 Mermaid HTML 页面]
E --> F[等待页面加载完成]
F --> G[注入 Mermaid 配置并执行渲染]
G --> H{检查输出格式}
H -- SVG --> I[提取 SVG XML 并保存为 .svg 文件]
H -- PNG --> J[计算 SVG 边界并截图保存为 .png 文件]
H -- PDF --> K[生成 PDF 并保存为 .pdf 文件]
I --> L[返回成功码 0]
J --> L
K --> L
L --> M[关闭浏览器]
M --> N[结束]
G -- 异常 --> O[记录错误日志]
O --> P[返回错误码 -1]
P --> M
mmdc_playwright.py
├── 全局函数
│ └── mermaid_to_file
└── 内部函数
└── console_message (事件处理函数)当前脚本文件所在的目录的绝对路径。
类型:str
Playwright异步上下文管理器实例,用于管理浏览器生命周期。
类型:async_playwright
启动的Chromium浏览器实例。
类型:Browser
设备像素比例,用于控制输出图像的分辨率缩放。
类型:float
浏览器上下文,用于隔离页面环境,包含视口和设备缩放因子等配置。
类型:BrowserContext
浏览器页面实例,用于加载HTML、执行JavaScript和捕获输出。
类型:Page
本地Mermaid渲染HTML文件的绝对路径。
类型:str
转换为file:协议的URL,用于在浏览器中加载本地HTML文件。
类型:str
Mermaid图表的配置字典,用于初始化渲染器。
类型:dict
图表背景颜色,默认为白色(#ffffff)。
类型:str
自定义CSS样式字符串,可注入到生成的SVG中。
类型:str
序列化后的SVG XML字符串,用于保存为.svg文件。
类型:str
包含SVG元素位置和尺寸的字典,用于精确截取PNG截图。
类型:dict
PNG格式的截图二进制数据。
类型:bytes
PDF格式的文档二进制数据。
类型:bytes
该函数是一个异步函数,用于将Mermaid代码转换为多种文件格式(如PNG、SVG、PDF)。它通过Playwright控制无头浏览器加载一个包含Mermaid渲染引擎的HTML页面,执行Mermaid代码,并将渲染结果截图或导出为指定格式的文件。
参数:
mermaid_code:str,需要转换的Mermaid代码。output_file_without_suffix:str,输出文件的名称(不包含后缀)。width:int,可选,输出图像的宽度,默认为2048。height:int,可选,输出图像的高度,默认为2048。suffixes:Optional[List[str]],可选,需要生成的文件后缀列表,支持"png"、"pdf"和"svg",默认为["png"]。
返回值:int,如果转换成功返回0,如果转换失败返回-1。
flowchart TD
A[开始] --> B[初始化参数<br>设置默认后缀为png]
B --> C[启动Playwright无头浏览器]
C --> D[创建浏览器上下文和页面]
D --> E[加载Mermaid HTML页面]
E --> F[等待页面加载完成]
F --> G[执行Mermaid渲染逻辑]
G --> H{是否生成SVG?}
H -- 是 --> I[提取SVG XML并保存为文件]
H -- 否 --> J{是否生成PNG?}
J -- 是 --> K[计算SVG裁剪区域并截图保存]
J -- 否 --> L{是否生成PDF?}
L -- 是 --> M[生成PDF并保存]
L -- 否 --> N[返回成功代码0]
I --> J
K --> L
M --> N
N --> O[关闭浏览器]
O --> P[结束]
style A fill:#f9f,stroke:#333,stroke-width:2px
style P fill:#f9f,stroke:#333,stroke-width:2px
async def mermaid_to_file(
mermaid_code, output_file_without_suffix, width=2048, height=2048, suffixes: Optional[List[str]] = None
) -> int:
"""Convert Mermaid code to various file formats.
Args:
mermaid_code (str): The Mermaid code to be converted.
output_file_without_suffix (str): The output file name without the suffix.
width (int, optional): The width of the output image. Defaults to 2048.
height (int, optional): The height of the output image. Defaults to 2048.
suffixes (Optional[List[str]], optional): The file suffixes to generate. Supports "png", "pdf", and "svg". Defaults to ["png"].
Returns:
int: 0 if the conversion is successful, -1 if the conversion fails.
"""
# 设置默认后缀为png,如果未提供suffixes参数
suffixes = suffixes or ["png"]
# 获取当前脚本所在目录的绝对路径
__dirname = os.path.dirname(os.path.abspath(__file__))
# 启动Playwright异步上下文管理器
async with async_playwright() as p:
# 启动Chromium浏览器
browser = await p.chromium.launch()
device_scale_factor = 1.0
# 创建浏览器上下文,设置视口大小和设备缩放因子
context = await browser.new_context(
viewport={"width": width, "height": height},
device_scale_factor=device_scale_factor,
)
# 创建新页面
page = await context.new_page()
# 定义控制台消息处理函数,用于日志记录
async def console_message(msg):
logger.info(msg.text)
# 监听页面控制台消息
page.on("console", console_message)
try:
# 设置页面视口大小
await page.set_viewport_size({"width": width, "height": height})
# 构建Mermaid HTML文件的路径和URL
mermaid_html_path = os.path.abspath(os.path.join(__dirname, "index.html"))
mermaid_html_url = urljoin("file:", mermaid_html_path)
# 导航到Mermaid HTML页面
await page.goto(mermaid_html_url)
# 等待页面网络空闲状态
await page.wait_for_load_state("networkidle")
# 等待容器元素加载完成
await page.wait_for_selector("div#container", state="attached")
mermaid_config = {}
background_color = "#ffffff"
my_css = ""
# 设置页面背景颜色
await page.evaluate(f'document.body.style.background = "{background_color}";')
# 执行JavaScript代码,渲染Mermaid图表
await page.evaluate(
"""async ([definition, mermaidConfig, myCSS, backgroundColor]) => {
const { mermaid, zenuml } = globalThis;
await mermaid.registerExternalDiagrams([zenuml]);
mermaid.initialize({ startOnLoad: false, ...mermaidConfig });
const { svg } = await mermaid.render('my-svg', definition, document.getElementById('container'));
document.getElementById('container').innerHTML = svg;
const svgElement = document.querySelector('svg');
svgElement.style.backgroundColor = backgroundColor;
if (myCSS) {
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.appendChild(document.createTextNode(myCSS));
svgElement.appendChild(style);
}
}""",
[mermaid_code, mermaid_config, my_css, background_color],
)
# 如果请求生成SVG文件
if "svg" in suffixes:
# 提取SVG元素的XML内容
svg_xml = await page.evaluate(
"""() => {
const svg = document.querySelector('svg');
if (!svg) {
throw new Error('SVG element not found');
}
const xmlSerializer = new XMLSerializer();
return xmlSerializer.serializeToString(svg);
}"""
)
# 记录日志并保存SVG文件
logger.info(f"Generating {output_file_without_suffix}.svg..")
with open(f"{output_file_without_suffix}.svg", "wb") as f:
f.write(svg_xml.encode("utf-8"))
# 如果请求生成PNG文件
if "png" in suffixes:
# 计算SVG元素的裁剪区域
clip = await page.evaluate(
"""() => {
const svg = document.querySelector('svg');
const rect = svg.getBoundingClientRect();
return {
x: Math.floor(rect.left),
y: Math.floor(rect.top),
width: Math.ceil(rect.width),
height: Math.ceil(rect.height)
};
}"""
)
# 调整视口大小以适应SVG元素
await page.set_viewport_size({"width": clip["x"] + clip["width"], "height": clip["y"] + clip["height"]})
# 截图并保存PNG文件
screenshot = await page.screenshot(clip=clip, omit_background=True, scale="device")
logger.info(f"Generating {output_file_without_suffix}.png..")
with open(f"{output_file_without_suffix}.png", "wb") as f:
f.write(screenshot)
# 如果请求生成PDF文件
if "pdf" in suffixes:
# 生成PDF数据并保存文件
pdf_data = await page.pdf(scale=device_scale_factor)
logger.info(f"Generating {output_file_without_suffix}.pdf..")
with open(f"{output_file_without_suffix}.pdf", "wb") as f:
f.write(pdf_data)
# 返回成功代码
return 0
except Exception as e:
# 记录错误并返回失败代码
logger.error(e)
return -1
finally:
# 确保浏览器被关闭
await browser.close()通过 Playwright 控制的 Chromium 浏览器,加载并执行 Mermaid.js 库,将输入的 Mermaid 代码渲染为 SVG 图形。
将渲染得到的 SVG 图形,通过截图或序列化操作,转换为 PNG、PDF 或 SVG 文件格式。
利用 Playwright 的异步 API 启动和管理浏览器实例,实现无头环境下的页面导航、脚本执行和元素操作。
支持向渲染过程注入自定义的 Mermaid 配置、CSS 样式和背景颜色,以控制图表的最终外观。
- 资源泄漏风险:代码中使用了
async with async_playwright() as p:来管理 Playwright 实例的生命周期,这确保了在函数结束时浏览器实例会被正确关闭。然而,如果在try块中发生异常,finally块中的await browser.close()仍然会执行,这是正确的。但需要注意,如果在browser = await p.chromium.launch()之后但在try块之前发生异常,browser变量可能为None,导致finally块中的await browser.close()调用失败。不过,当前代码结构下,browser的初始化在try块外,如果此处失败,函数会直接退出,不会进入finally块,因此风险较低。主要风险在于context和page对象没有显式的close调用,它们依赖于browser.close()来清理。在复杂的异步场景中,这通常是足够的,但并非最佳实践。 - 硬编码的 HTML 文件路径:函数依赖于一个硬编码的本地 HTML 文件路径 (
index.html) 来渲染 Mermaid 图表。这降低了代码的可移植性和可测试性,因为该文件必须存在于特定的相对位置。如果文件被移动、重命名或缺失,函数将无法工作。 - 缺乏输入验证:函数没有对输入参数(如
mermaid_code,suffixes)进行充分的验证。例如,空的mermaid_code可能导致页面渲染空白或错误;suffixes列表中包含不支持的格式(如"jpg")时,函数会忽略它,但不会给出警告或错误。 - 同步文件写入:在异步函数中使用了同步的
with open(...) as f:进行文件写入。虽然对于单个文件操作影响可能不大,但在高并发场景下,这可能会阻塞事件循环,影响性能。 - 错误处理过于笼统:
except Exception as e:捕获了所有异常,虽然记录了错误日志,但返回的-1对于调用者来说信息量不足,无法区分是网络问题、渲染问题、文件写入问题还是其他问题。 - 潜在的竞态条件:在生成 PNG 时,代码先通过 JavaScript 计算 SVG 的边界框 (
clip),然后调整视口大小,最后截图。如果在计算clip和截图之间,页面内容发生了变化(虽然在此特定场景中可能性很小),可能会导致截图不准确或失败。 - 设备缩放因子处理:
device_scale_factor被硬编码为1.0,并且在生成 PDF 时使用了这个值。这可能导致在高DPI屏幕上生成的PDF尺寸与预期不符。虽然视口大小通过width和height参数控制,但device_scale_factor未根据实际需求或系统设置进行配置。
- 显式清理资源:在
try块内部,成功创建context和page后,可以考虑在finally块中先显式关闭page和context,再关闭browser。虽然browser.close()通常会清理其子资源,但显式关闭可以更清晰地表达资源生命周期管理意图,并避免在某些边缘情况下的资源泄漏。例如:finally: if page: await page.close() if context: await context.close() if browser: await browser.close()
- 解耦 HTML 模板依赖:将 HTML 模板内容(
index.html)作为字符串嵌入到 Python 代码中,或者通过参数、配置文件指定模板路径。这样可以提高代码的独立性和可测试性。例如,可以读取该文件内容,然后通过page.set_content()加载,而不是依赖file://URL。 - 增强输入验证:在函数开始时验证关键参数。例如,检查
mermaid_code是否非空且为字符串;检查suffixes列表中的每个元素是否属于{"png", "svg", "pdf"},对于无效项可以记录警告或引发明确的ValueError。 - 使用异步文件操作:考虑使用
aiofiles库或其他异步文件写入方法,以保持整个操作流程的异步性,避免阻塞事件循环。 - 细化错误处理与返回信息:可以定义自定义异常类或使用更具体的异常捕获(如
playwright._impl._errors.Error,IOError),并根据异常类型返回不同的错误码或包含错误信息的元组/对象,方便调用者处理。 - 优化截图流程的原子性:为了确保截图时页面状态稳定,可以在计算
clip和执行page.screenshot之间,通过page.evaluate执行一个简单的同步操作或检查,以确保DOM没有在异步更新。或者,考虑使用page.wait_for_function来等待某个稳定状态。 - 提供设备缩放因子配置:将
device_scale_factor作为函数的一个可选参数,允许调用者根据输出需求(如生成高分辨率图片)进行设置。同时,可以尝试自动检测系统或环境的缩放因子。 - 代码结构与可读性:当前函数较长,混合了浏览器控制、页面交互、文件输出等多种逻辑。可以考虑将其拆分为几个更小的、功能单一的辅助函数,例如
_render_mermaid_to_svg,_save_svg_to_file,_take_screenshot,_generate_pdf等,以提高可读性、可测试性和可维护性。 - 增加超时控制:为页面导航 (
page.goto)、等待选择器 (page.wait_for_selector)、渲染评估 (page.evaluate) 等操作添加超时参数,防止因网络问题或脚本错误导致函数长时间挂起。 - 支持更多输出格式或配置:评估是否需要支持更多 Mermaid 配置选项(通过
mermaid_config参数),或者是否允许用户传入自定义的 CSS 样式 (my_css) 来更精细地控制输出样式。
该模块的核心设计目标是将 Mermaid 图表代码转换为多种静态文件格式(SVG、PNG、PDF)。主要约束包括:1) 依赖 Playwright 无头浏览器进行渲染,以确保图表渲染的准确性和与 Web 环境的一致性;2) 需要本地 HTML 文件作为渲染模板,存在外部文件依赖;3) 转换过程为异步操作,需适应异步编程模型;4) 输出文件尺寸由输入参数动态控制,需确保渲染视口与截图区域匹配。
模块的错误处理策略较为集中。mermaid_to_file 函数使用一个顶层的 try-except 块捕获所有执行过程中的异常。当发生任何异常时,会记录错误日志并返回状态码 -1 表示失败。成功执行则返回 0。这种设计简化了错误处理,但可能掩盖具体的错误类型,不利于调用者进行精细化处理。例如,文件写入失败、浏览器启动失败、页面元素未找到等异常均被统一归类为通用失败。
- 输入:Mermaid 代码字符串、输出文件基础名、图像尺寸参数、目标格式列表。
- 处理流程:
- 初始化:启动 Playwright 浏览器实例,创建指定尺寸的页面上下文。
- 加载环境:导航至本地 HTML 模板文件,等待页面资源加载完成。
- 渲染图表:通过
page.evaluate将 Mermaid 代码注入页面,调用 Mermaid.js 库进行渲染,生成 SVG 元素。 - 输出生成:根据
suffixes列表,循环或条件判断生成不同格式文件:- SVG:序列化页面中的 SVG 元素并写入文件。
- PNG:计算 SVG 元素的位置和尺寸,对特定区域进行截图并写入文件。
- PDF:将整个页面导出为 PDF 并写入文件。
- 输出:一个或多个图像/文档文件(
.svg,.png,.pdf)及一个整数状态码。 - 状态:流程基本上是线性的,没有复杂的状态切换。主要状态节点包括:浏览器未启动、浏览器就绪、页面加载中、图表渲染完成、文件写入中、流程结束(成功或失败)。
- 库依赖:
playwright: 用于控制无头浏览器进行页面渲染和截图/PDF生成。核心依赖其async_api。metagpt.logs: 项目内部日志模块,用于记录过程信息和错误。
- 文件依赖:
- 同目录下的
index.html文件:作为渲染 Mermaid 图表的宿主页面模板。此文件必须存在且包含 Mermaid.js 及其相关依赖(如 zenuml)的引入。
- 同目录下的
- 环境依赖:
- 需要安装 Playwright 的 Chromium 浏览器驱动(可通过
playwright install chromium安装)。 - 运行环境需支持异步 I/O。
- 需要安装 Playwright 的 Chromium 浏览器驱动(可通过
- 接口契约:
- 函数签名:
async def mermaid_to_file(mermaid_code, output_file_without_suffix, width=2048, height=2048, suffixes: Optional[List[str]] = None) -> int - 调用约定:调用者必须
await此异步函数。output_file_without_suffix应包含完整的目录路径(如果不在当前目录)。函数不负责创建目录。 - 参数约束:
suffixes列表中的字符串应为小写的"png","svg","pdf",函数内部未做严格校验,传入不支持的后缀可能导致无输出。
- 函数签名:
- 本地文件访问:通过
file:协议加载本地 HTML 文件,这在现代浏览器中通常受到严格限制(CORS、同源策略)。Playwright 上下文默认可能放宽这些限制,但此行为依赖于 Playwright 的配置,存在环境差异性。 - 代码注入:函数将用户提供的
mermaid_code作为参数传递给页面内的evaluate方法执行。虽然evaluate的调用方式本身是参数化传递(非字符串拼接),减少了直接脚本注入风险,但mermaid_code内容最终会在浏览器环境中作为 Mermaid 定义被解析和执行。如果 Mermaid.js 库本身存在漏洞,恶意构造的 Mermaid 代码可能带来风险。目前设计假设输入代码是可信的。 - 资源清理:使用
async with和finally块确保浏览器实例在任何情况下(成功、异常)都会被关闭,避免了资源泄漏。