From 55ded6a2c2becb11ca02486320ab2cf159ee78ae Mon Sep 17 00:00:00 2001 From: liuwei Date: Fri, 27 Feb 2026 10:02:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E4=B8=8B=20markdown?= =?UTF-8?q?=5Fto=5Fimage.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/markdown_to_image.py | 43 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index b8af4df..e45b9f8 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -8,6 +8,38 @@ import asyncio from loguru import logger +async def safe_close_browser(browser, timeout: float = 5.0) -> None: + if not browser: + return + + # Close contexts first to reduce hanging risk. + try: + for ctx in browser.contexts: + try: + await asyncio.wait_for(ctx.close(), timeout=timeout) + except Exception: + pass + except Exception: + pass + + # Then close browser with timeout. + try: + await asyncio.wait_for(browser.close(), timeout=timeout) + return + except asyncio.TimeoutError: + logger.warning("Browser close timed out, killing process") + except Exception as e: + logger.warning(f"Browser close failed, killing process: {e}") + + # Final fallback: kill underlying process. + try: + proc = browser.process + if proc: + proc.kill() + except Exception: + pass + + # ================= 样式与 HTML 处理 ================= async def md_str_to_html_content(md_content): @@ -99,23 +131,28 @@ async def html_to_image(html_content, output_image): ] if browser_path: + logger.debug(f"Launch chromium with system chrome: {browser_path}") browser = await p.chromium.launch(executable_path=browser_path, args=launch_args) else: + logger.debug("Launch chromium with bundled browser") browser = await p.chromium.launch(args=launch_args) try: # 使用更高的 device_scale_factor 可以让图片更清晰 - context = await browser.new_context(device_scale_factor=2) + context = await browser.new_context(device_scale_factor=1) page = await context.new_page() # 3. 动态调整高度:先探测内容实际高度 + logger.debug("Measure body height") body_height = await page.evaluate("document.body.scrollHeight") await page.set_viewport_size({"width": 750, "height": body_height}) # 2. 【关键】强制等待所有字体和 Emoji 加载完成 # 很多时候卡住就是在等字体渲染计算 + logger.debug("Wait for fonts ready") await page.evaluate("document.fonts.ready") # 【优化核心】:直接设置 HTML 内容,不走 file:// 协议 # 这样可以彻底避免文件读取超时 + logger.debug("Set page content") await page.set_content(html_content, wait_until='load') # 稍微等待一下确保 CSS 渲染完成 @@ -123,6 +160,7 @@ async def html_to_image(html_content, output_image): # 截图(full_page=True 会自动处理高度) # 4. 截图增加超时限制,防止死锁 + logger.debug(f"Take screenshot: output={output_image}, height={body_height}") await page.screenshot( path=output_image, full_page=True, @@ -133,7 +171,8 @@ async def html_to_image(html_content, output_image): raise RuntimeError(f"截图失败,输出文件不存在: {output_image}") finally: - await browser.close() + logger.debug("Closing browser") + await safe_close_browser(browser) # ================= 主转换函数 =================