Back to skills
extension
Category: OtherNo API key required

Boo哥AI-html转pdf

Boo哥AI网页转PDF — 将在线幻灯片、Axure原型、演示页面自动转为PDF。 当用户提供网页URL并要求转成PDF、导出为PDF、截图保存、网页转文档时使用。 触发场景:在线原型转PDF(axureshow.com等)、HTML幻灯片转PDF、 单页演示页面截图合成PDF、带圆点/缩略图导航的网页导出。 当用户提到"网页转PDF"、"在线原型导出"、"页面截图合成PDF"、 "把这个链接转成PDF"、"axure转pdf"、"幻灯片导出pdf"等关键词时优先使用。

personAuthor: user_3c9003afhubcommunity

BooAi-web-to-pdf — 网页转PDF引擎

Boo哥AI智写 · 专业AI标书写作引擎 | 在线页面 → PDF 转换工具

概述

将在线幻灯片/原型页面自动转为 PDF。核心思路:Playwright 无头浏览器逐页截图 + img2pdf 无损合成。

适用场景

  • 在线 Axure 原型导出(axureshow.com 等平台)
  • HTML 幻灯片/演示文稿(圆点导航、缩略图导航)
  • 单页 Web 应用逐页导出
  • 任何可通过点击导航逐页切换的网页

不适用

  • 需要登录认证的页面
  • 无限滚动加载的页面
  • 纯静态无导航的单页

工作流程

第一步:探索页面结构

用 Playwright 访问 URL,分析导航方式:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--ignore-certificate-errors"])
    page = browser.new_page(viewport={"width": 1920, "height": 1080})
    page.goto(url, wait_until="commit", timeout=30000)
    page.wait_for_timeout(5000)

    # 检测导航类型
    dots = page.evaluate("() => document.querySelectorAll('#nav .dot, .dot, [class*=pagination] button').length")
    text = page.evaluate("() => document.body.innerText.substring(0, 500)")
    print(f"圆点导航数: {dots}")
    print(f"页面文本片段: {text}")
    print(f"页面标题: {page.title()}")
    browser.close()

常见导航模式识别: | 模式 | 选择器示例 | 切换方式 | |------|-----------|---------| | 圆点按钮 | #nav .dot[data-i] | .click() | | 侧边栏树 | .sitemapPage, [data-pageid] | .click() 后等 iframe 加载 | | 左右箭头 | .arrow-left, .arrow-right, [class*=arrow] | .click() | | 键盘翻页 | 无可见导航 | page.keyboard.press("ArrowRight") |

第二步:编写转换脚本

核心模板 — 根据第一步识别的导航模式选择:

from playwright.sync_api import sync_playwright
from PIL import Image
import img2pdf, os

URL = "<目标URL>"
OUTPUT = "<输出PDF路径>"
TEMP = "temp_slides"
os.makedirs(TEMP, exist_ok=True)

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--ignore-certificate-errors"])
    context = browser.new_context(viewport={"width": 1920, "height": 1080}, device_scale_factor=2)

    # ★ 关键:劫持字体系统防止 screenshot 超时
    context.add_init_script("""
        const _FontFace = window.FontFace;
        window.FontFace = function(family, source, descriptors) {
            const ff = new _FontFace(family, source, descriptors);
            Object.defineProperty(ff, 'status', { get: () => 'loaded' });
            ff.load = () => Promise.resolve(ff);
            return ff;
        };
        Object.defineProperty(document, 'fonts', {
            get: () => ({ ready: Promise.resolve(), add: () => {}, clear: () => {} }),
            configurable: true,
        });
    """)

    page = context.new_page()

    # 阻断外部字体下载
    page.route("**/*", lambda route: (
        route.abort() if any(x in route.request.url.lower() for x in
            ['.woff', '.ttf', '.otf', '.eot', 'fonts.googleapis.com', 'fonts.gstatic.com'])
        else route.continue_()
    ))

    page.goto(URL, wait_until="commit", timeout=30000)
    page.wait_for_timeout(5000)

    # 隐藏导航UI元素
    page.evaluate("""() => {
        const nav = document.getElementById('nav');
        if (nav) nav.style.display = 'none';
        document.querySelectorAll('.snap-close, .ai-highlight, .chrome, [class*=toolbar]').forEach(el => el.style.display = 'none');
    }""")

    # 获取页数(此处以圆点导航为例,其他模式需调整)
    total = page.evaluate("() => document.querySelectorAll('#nav .dot').length")
    print(f"共 {total} 页")

    screenshots = []
    for i in range(total):
        page.evaluate(f"document.querySelectorAll('#nav .dot')[{i}].click()")
        page.wait_for_timeout(1200)
        path = os.path.join(TEMP, f"slide_{i:03d}.png")
        page.screenshot(path=path, full_page=False, timeout=60000)
        screenshots.append(path)
        print(f"  [{i+1}/{total}]")

    browser.close()

# 合成 PDF(用 img2pdf,不用 Pillow — Pillow 可能缺 JPEG 编码器)
with open(OUTPUT, 'wb') as f:
    f.write(img2pdf.convert(screenshots))

# 清理
for p in screenshots:
    os.remove(p)
os.rmdir(TEMP)
print(f"完成: {OUTPUT}")

第三步:验证与交付

  1. 打开生成的 PDF,确认页数正确
  2. 检查第一页和最后一页内容是否完整
  3. 若文件过大(>30MB),可降 device_scale_factor 为 1

关键陷阱

陷阱 1:screenshot 字体加载超时

症状Page.screenshot: Timeout 30000ms exceeded. waiting for fonts to load...

根因:Playwright 内部在截图前等待 document.fonts.ready,国内访问 Google Fonts 等 CDN 超时导致永远无法 ready。

修复:必须在 context.add_init_script 中劫持 FontFace 构造函数和 document.fonts 对象(见上面模板),仅靠 page.route 阻断网络请求不够,因为字体可能在路由注册前已开始加载。

陷阱 2:Pillow PDF 合成报 KeyError: 'JPEG'

症状KeyError: 'JPEG'PdfImagePlugin.py

根因:Windows 上通过 pip 安装的 Pillow wheel 可能未编译 JPEG 编码器。

修复:使用 img2pdf 库替代 Pillow 的 Image.save() 进行 PDF 合成。img2pdf 是纯 Python 实现,不依赖外部编解码器。

pip install img2pdf playwright
playwright install chromium

陷阱 3:页面加载超时

症状Page.goto: Timeout 30000ms exceeded

修复

  • wait_until="commit" 代替 "networkidle"(后者要求 500ms 内无网络请求)
  • 手动 wait_for_timeout(5000) 等待 JS 渲染完成
  • 若仍超时,检查是否需要代理或网络连通性

依赖

| 依赖 | 用途 | 安装 | |------|------|------| | Python 3.9+ | 运行环境 | 系统 | | playwright | 无头浏览器截图 | pip install playwright | | chromium (playwright) | 浏览器内核 | playwright install chromium | | img2pdf | 无损 PDF 合成 | pip install img2pdf |

脚本位置

可复用转换脚本位于:scripts/web_to_pdf.py

python scripts/web_to_pdf.py <URL> [--output out.pdf] [--scale 1|2]

版本

[v1.0.0] - 2026-05-26 · 初始版本 · CHANGELOG


<p align="center"> <strong>Powered by Boo哥AI智写</strong><br> 📧 联系与反馈:<a href="mailto:409966830@qq.com">409966830@qq.com</a><br> <sub>智写万象,标定未来</sub> </p>