如何让Puppeteer生成的PDF从第二页开始显示页码?
实现Puppeteer生成带封面的PDF(页码从第二页开始)
需求与问题
需要生成单个PDF文件,第一页作为封面不显示页码,从第二页开始页码从1计数。尝试过addScriptTag、page.evaluate修改内置pageNumber、CSS控制、页码减1等方法均无效,因Chromium默认页码从1开始,内置pageNumber难以直接修改。
可行解决方案
方案1:CSS+页眉脚本动态调整(单页生成)
核心思路:通过CSS隐藏第一页的页眉,同时在页眉模板中注入脚本,将页码数值减1(第一页直接隐藏页码元素)。
修改步骤:
- 调整页面样式,给第一页单独设置页眉隐藏规则
- 在页眉模板中添加脚本,动态计算并修改页码
- 保持原有PDF生成逻辑,调整部分配置
修改后的核心代码:
// 修改page.addStyleTag部分,添加封面页页眉隐藏规则 await page.addStyleTag({ content: ` body { margin-top: 1cm; } @page:first { margin-top: 0; /* 清空第一页的页眉区域内容 */ @top-left { content: ""; } @top-center { content: ""; } @top-right { content: ""; } } `, }); // 修改headerTemplate,添加动态调整页码的脚本 headerTemplate: ` ${styleContent} <script> window.onload = function() { const pageNumEl = document.querySelector('.pageNumber'); const currentPage = parseInt(pageNumEl.textContent); if (currentPage > 1) { // 第二页开始页码从1计数 pageNumEl.textContent = currentPage - 1; } else { // 第一页隐藏页码行 pageNumEl.parentElement.style.display = 'none'; } } </script> <div class='invoice-code'> <div style='width:40%; font-size: 10px;'></div> <div style='width:30%; font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>PURCHASE ORDER</div> <div style='width:30%; font-size: 10px; line-height:1.2;'> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Number<span style="font-weight: normal;"> : ${payloadJSON.po_number}</span></div> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Po Date<span style="font-weight: normal;"> : ${payloadJSON.po_date}</span></div> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Page No <span style="font-weight: normal;">:</span> <span style="font-weight: normal;" class="pageNumber"> </span></div> </div> </div> `,
方案2:分生成再合并(更稳定)
如果方案1因Chromium的页眉渲染机制限制失效,可以采用分两次生成PDF再合并的方式,彻底规避页码计数问题:
步骤:
- 安装PDF合并库
pdf-lib:npm install pdf-lib - 分别生成封面PDF(无页码)和内容PDF(页码从1开始)
- 合并两个PDF文件
修改后的完整代码:
const puppeteer = require('puppeteer'); const hbs = require('handlebars'); const fs = require('fs'); const path = require('path'); const { PDFDocument } = require('pdf-lib'); // 引入pdf-lib const data = require('./data.json'); const ThermalPrinterHelper = require('./ThermalPrinterHelper'); const { generateQrCode, generateBarCode } = require('./generator'); hbs.registerHelper('inc', function (value, options) { return parseInt(value) + 1; }); const compile = async function (template, data) { const filePath = path.join(__dirname, 'templates', `${template}.hbs`); const templateSource = await fs.promises.readFile(filePath, 'utf-8'); return hbs.compile(templateSource)(data); }; const styleContent = ` <style> .invoice-code { display: flex; justify-content: space-between; align-items: center; width: 100%; min-height: 100px; font-size: 10px; } </style> `; function groupBySubtotalQuantity(poItems) { const groupedPayload = {}; poItems.forEach((item) => { const groupKey = item.article_no.toString().substring(0, 9); const totalQuantity = parseFloat(item.quantity); if (!groupedPayload[groupKey]) { groupedPayload[groupKey] = { total_quantity: 0, items: [], }; } groupedPayload[groupKey].items.push(item); groupedPayload[groupKey].total_quantity += totalQuantity; }); Object.keys(groupedPayload).forEach((key) => { groupedPayload[key].total_quantity = parseFloat( groupedPayload[key].total_quantity.toFixed(3) ).toFixed(3); }); Object.keys(groupedPayload).forEach((key) => { groupedPayload[key].items.sort((a, b) => { const getLastNumber = (str) => parseInt(str.match(/\d+$/)[0]); return getLastNumber(a.material_description) - getLastNumber(b.material_description); }); }); const result = Object.keys(groupedPayload).map((key) => groupedPayload[key]); return { po_item: result }; } // 生成封面PDF(无页码) const generateCoverPDF = async (page, payload) => { const coverContent = await compile('cover', payload); // 需提前创建cover.hbs封面模板 await page.setContent(coverContent); return await page.pdf({ format: 'A4', printBackground: true, displayHeaderFooter: false, // 封面不显示页眉页脚 }); }; // 生成内容PDF(页码从1开始) const generateContentPDF = async (page, payload) => { const content = await compile('index', payload); await page.setContent(content); await page.addStyleTag({ content: `body { margin-top: 1cm; }`, }); return await page.pdf({ format: 'A4', printBackground: true, preferCSSPageSize: true, displayHeaderFooter: true, margin: { top: '100px', }, headerTemplate: ` ${styleContent} <div class='invoice-code'> <div style='width:40%; font-size: 10px;'></div> <div style='width:30%; font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>PURCHASE ORDER</div> <div style='width:30%; font-size: 10px; line-height:1.2;'> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Number<span style="font-weight: normal;"> : ${payload.po_number}</span></div> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Po Date<span style="font-weight: normal;"> : ${payload.po_date}</span></div> <div style=' font-size: 10pt; font-family:Helvetica, sans-serif; font-style: normal; font-weight: bold;'>Page No <span style="font-weight: normal;">:</span> <span style="font-weight: normal;" class="pageNumber"> </span></div> </div> </div> `, footerTemplate: ` ${styleContent} <div class='invoice-code'> <div style='width:40%; font-size: 12px;'></div> </div> `, }); }; // 合并两个PDF const mergePDFs = async (coverBytes, contentBytes) => { const coverDoc = await PDFDocument.load(coverBytes); const contentDoc = await PDFDocument.load(contentBytes); const mergedDoc = await PDFDocument.create(); // 复制封面页 const coverPages = await mergedDoc.copyPages(coverDoc, coverDoc.getPageIndices()); coverPages.forEach(page => mergedDoc.addPage(page)); // 复制内容页 const contentPages = await mergedDoc.copyPages(contentDoc, contentDoc.getPageIndices()); contentPages.forEach(page => mergedDoc.addPage(page)); return mergedDoc.save(); }; const generatePDF = async () => { try { const browser = await puppeteer.launch({ args: ['--no-sandbox'], devtools: true, }); const page = await browser.newPage(); const payloadJSON = data; const groupedPayload = groupBySubtotalQuantity(payloadJSON.po_item); payloadJSON.po_item = groupedPayload.po_item; // 生成封面和内容PDF const coverBytes = await generateCoverPDF(page, payloadJSON); const contentBytes = await generateContentPDF(page, payloadJSON); // 合并并保存 const mergedBytes = await mergePDFs(coverBytes, contentBytes); await fs.promises.writeFile('output.pdf', mergedBytes); console.log('PDF generated successfully'); await browser.close(); } catch (e) { console.log(e); } }; generatePDF();
内容的提问来源于stack exchange,提问作者Vivek Gupta
相关产品推荐
相关产品推荐

