puppeteer生成pdf与截图
之前写过一篇 vue cli2 使用 wkhtmltopdf 踩坑指南,由于wkhtmltopdf对vue的支持并不友好,而且不支持css3,经过调研最终选择puppeteer,坑少,比较靠谱。
一、准备工作
- puppeteer中文文档: https://zhaoqize.github.io/puppeteer-api-zh_CN/#/
- node版本必须在10.18.1+以上
- 新建pdf.js
- 安装puppeteer
npm install puppeteer
(这里用的是15.0.1版本,测试没问题) - 需要生成pdf的html页面需要添加打印样式(不添加会导致背景色无法显示等问题)
html { -webkit-print-color-adjust: exact; } - cd到pdf.js所在的文件夹执行node pdf.js
补充有关打印样式的css编写(也可以用puppeteer控制生成的边距):
/* 媒体查询 */ @media print{ .noprint{ display:none; } } /* 打印页面边距设置 */ @page { margin: 2cm 0 0 0; } /* 第一页边距设置 */ @page :first { margin: 0; }
二、常用案例
这里直接提供一些常用的生成pdf案例,比较简单,直接复制就能用
1. 通过设置token下载pdf的最简单使用方式
这种方式用于直接访问页面下载pdf,可以给页面所有http请求设置headers,直接执行 node pdf.js 即可生成pdf
pdf.js
const puppeteer = require('puppeteer') const token = 'kjjkheyJzdWIiOiIxMDAwMDAwMDAwMDAxMjM0' async function printPDF() { const browser = await puppeteer.launch({ headless: true }) const page = await browser.newPage() await page.setExtraHTTPHeaders({'uniedu-sso-token': token}) await page.goto('https://baidu.com', {waitUntil: 'networkidle0'}) await page.pdf({ path: 'test.pdf', format: 'A4'}) await browser.close() } printPDF()
2. 通过html字符串生成pdf
pdf.js
const puppeteer = require('puppeteer') const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />· <title>Document</title> <style> html { line-height: 1.15; -webkit-print-color-adjust: exact; } body { margin: 0; font-family: "Times New Roman",'宋体'; font-weight: 400; } </style> </head> <body> <div>页面Dom</div> </body> </html>` async function printPDF() { const browser = await puppeteer.launch({ headless: true }) const page = await browser.newPage() await page.setContent(html, {waitUntil: 'networkidle0'}) await page.pdf({ path: 'test.pdf', format: 'A4'}) await browser.close() } printPDF()
3. 简单封装node命令的形式并通过引入html文件生成pdf
首先需要安装 npm install minimist
这里的A3自定义了宽高,puppeteer也有自己默认的A3尺寸,具体详见官方文档page.pdf([options])
生成test.pdf可通过执行该命令 node pdf.js --format=A3 --htmlPath=./index.html --pdfPath=./test.pdf
const puppeteer = require('puppeteer') const fs = require('fs') const args = require('minimist')(process.argv.slice(2)) const format = args['format'] const htmlPath = args['htmlPath'] const pdfPath = args['pdfPath'] const pdfParams = { 'A3': { path: pdfPath, width: '420mm', height: '297mm', margin: { right: '0.1cm', } }, 'A4': { path: pdfPath, format: 'A4' } } async function printPDF(format = 'A4') { // 如果需要部署在服务端尽量加上这行参数 const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: true}) const page = await browser.newPage() const htmlContent = fs.readFileSync(htmlPath, 'utf-8') await page.setContent(htmlContent, { waitUntil: 'networkidle0' }) await page.pdf(pdfParams[format]) await browser.close() } printPDF(format)
4. 循环异步批量下载pdf
这里browser只需要打开一次就可以了,只需要每次跳转新页面下载pdf,这样可以不用频繁的开启关闭无头浏览器
const puppeteer = require('puppeteer') const tokens = require('./tokens.json') async function printPDF(page, token, index) { console.log(`第${index + 1}份正在打印……`) await page.goto('https://baidu.com', {waitUntil: 'networkidle0'}); await page.pdf({ path: `./pdf/node${index + 1}.pdf`, format: 'A4'}) console.log(`第${index + 1}份已经完成`) } async function printAll() { console.log(`一共${tokens.length}份,正在打印中……`) const browser = await puppeteer.launch({ headless: true }) const page = await browser.newPage() await page.setExtraHTTPHeaders({'uniedu-sso-token': token}) for(let i = 0; i < tokens.length; i ++){ await printPDF(page, tokens[i], i) } await browser.close() } printAll()
如果需要自动生成文件夹归类,可以用node的fs.existsSync和fs.mkdirSync方法,先判断有没有这个文件夹,没有则创建
if(!fs.existsSync('dirName')) { fs.mkdirSync('dirName') }
案例
users.json
[ { "accountId": 1000005000000010244, "className": "701班", "userName": "陈昱含", "token": "eyJhbGciOiJSUzI1NiJ9" }, { "accountId": 1000005000000010245, "className": "701班", "userName": "韩艺臻", "token": "eyJhbGciOiJSUzI1NiJ9" }, { "accountId": 1000005000000010246, "className": "702班", "userName": "范玥", "token": "eyJhbGciOiJSUzI1NiJ9" }, { "accountId": 1000005000000010247, "className": "702班", "userName": "方泽清", "token": "eyJhbGciOiJSUzI1NiJ9" }, { "accountId": 1000005000000010248, "className": "702班", "userName": "付明宇", "token": "eyJhbGciOiJSUzI1NiJ9" } ]
pdfs.js
const fs = require('fs') const puppeteer = require('puppeteer') const users = require('./users.json') ;(async () => { const pdfPath = './pdfs/' // 处理token数据 const nameArr = [...new Set(users.map(v => v.className))] const classes = nameArr.map(className => ({ className, dirName: pdfPath + className, students: users.filter(v => v.className === className).map(v => ({ ...v, dirName: `${pdfPath + className}/${v.userName}` })) })) // 创建无头浏览器 const browser = await puppeteer.launch({ headless: true }) const page = await browser.newPage() let count = 0 for (const cla of classes) { if (!fs.existsSync(cla.dirName)) fs.mkdirSync(cla.dirName) for (const stu of cla.students) { if (!fs.existsSync(stu.dirName)) fs.mkdirSync(stu.dirName) await page.setExtraHTTPHeaders({ 'uniedu-sso-token': stu.token }) await page.goto('http://localhost:8080/#/report?07=1000100000006089002', { waitUntil: 'networkidle0' }) await page.pdf({ path: `${stu.dirName}/学情分析.pdf`, format: 'A4' }) console.log(`第${++count}份 -> ${cla.className} -> ${stu.userName} -> 已完成……`) } } await browser.close() })()
5. puppeteer生成截图
这里直接给出最近简单封装的node命令形式的代码作为参考,大部分参数可以参考官方文档
唯一值得说的一个参数是fitContent,这个是我自己加的,可以用于局部的截图,需要html的标签内含有screenshot
这个id,说白了就是需要截图的元素用<div id="screenshot"></div>
包裹起来
const puppeteer = require('puppeteer') const fs = require('fs') const args = require('minimist')(process.argv.slice(2)) const clip = {} args.clipX && (clip.x = Number(args.clipX)) args.clipY && (clip.y = Number(args.clipY)) args.clipW && (clip.width = Number(args.clipW)) args.clipH && (clip.height = Number(args.clipH)) let params = {} args.imgPath && (params.path = args.imgPath) args.type && (params.type = args.type) args.quality && (params.quality = Number(args.quality)) args.fullPage && (params.fullPage = args.fullPage === 'true') args.omitBackground && (params.omitBackground = args.omitBackground === 'true') args.encoding && (params.encoding = args.encoding) Object.keys(clip).length !== 0 && (params.clip = clip) async function printImg() { const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox'], headless: true}) const page = await browser.newPage() const htmlContent = fs.readFileSync(args.htmlPath, 'utf-8') await page.setContent(htmlContent, { waitUntil: 'networkidle0' }) const range = await page.$('#screenshot') const clip = await range.boundingBox() const result = args.fitContent === 'true' ? { ...params, clip } : params await page.screenshot(result) await browser.close() } printImg() /* 参数说明: htmlPath: html文件路径 imgPath: 截图保存路径。截图图片类型将从文件扩展名推断出来。如果是相对路径,则从当前路径解析。如果没有指定路径,图片将不会保存到硬盘 type: 指定截图类型, 可以是 jpeg 或者 png。默认 'png'. quality: 图片质量, 可选值 0-100. png 类型不适用。 fullPage: 如果设置为true,则对完整的页面(需要滚动的部分也包含在内)。默认是false clipX: 指定裁剪区域相对于左上角(0, 0)的x坐标 clipY: 指定裁剪区域相对于左上角(0, 0)的y坐标 clipW: 指定裁剪区域的宽度 clipH: 指定裁剪区域的高度 omitBackground: 隐藏默认的白色背景,背景透明。默认不透明 encoding: 图像的编码可以是 base64 或 binary。 默认为“二进制”。 fitContent: 设为true,则只对id="screenshot"包裹的内容区域截图 */ // node 命令示例 // node pdf.js --htmlPath=./index.html --imgPath=aa.png --fitContent=true
6. 调用本地chrome
puppeteer默认会安装一个最新版本的chromiue,也可以调起本地的chrome,这时候需要使用puppeteer-core
这个find_chrome.js在mac上测试没问题,但是windows上不好使,可以直接找到chrome安装路径直接复制过来,注意windows复制的路径是反斜杠要改成斜杠。
安装npm i puppeteer-core carlo
const puppeteer = require('puppeteer-core'); //find_chrome模块来源于GoogleChromeLabs的Carlo,可以查看本机安装Chrome目录 const findChrome = require('./node_modules/carlo/lib/find_chrome.js') ;(async () => { let findChromePath = await findChrome({}) let executablePath = findChromePath.executablePath; console.log(executablePath) const browser = await puppeteer.launch({ executablePath, headless: false }) const page = await browser.newPage() await page.goto('https://www.baidu.com/') // await browser.close() })()
添加水印
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://baidu.com'); await page.addStyleTag({ content: ` body { position: relative; } .watermark { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 48px; opacity: 0.5; color: #ccc; pointer-events: none; } `, }); await page.evaluate(() => { const watermark = document.createElement('div'); watermark.className = 'watermark'; watermark.innerText = 'Watermark Text'; document.body.appendChild(watermark); }); await page.pdf({ path: 'test.pdf', format: 'A4', printBackground: true, }); await browser.close(); })();
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律