PDF处理工作流完全指南:合并、分割、压缩与转换
PDF(Portable Document Format)是最通用的文档格式。无论是合同签署、报告分享还是电子书阅读,PDF都是首选。本文将系统介绍PDF处理的各种技术和最佳实践。
目录
PDF基础知识
PDF的特点
优势:
- ✅ 跨平台兼容性好
- ✅ 保持原始排版
- ✅ 支持加密和签名
- ✅ 文件大小可控
- ✅ 可嵌入字体和图片
劣势:
- ❌ 编辑不如Word方便
- ❌ 文件可能较大
- ❌ 复制文字可能格式混乱
PDF文件结构
PDF文件结构
├── Header(文件头)- PDF版本信息
├── Body(主体)
│ ├── 页面对象
│ ├── 内容流
│ ├── 字体
│ ├── 图片
│ └── 其他资源
├── Cross-Reference Table(交叉引用表)
└── Trailer(文件尾)
PDF版本
// 常见PDF版本 const pdfVersions = { 1.3: 'Acrobat 4', // 1999年 1.4: 'Acrobat 5', // 2001年,支持透明度 1.5: 'Acrobat 6', // 2003年,支持压缩 1.6: 'Acrobat 7', // 2005年 1.7: 'Acrobat 8+', // 2006年,最常用 '2.0': 'ISO 32000-2', // 2017年,最新标准 };
浏览器端PDF处理
使用PDF.js渲染PDF
// PDF.js是Mozilla开发的开源PDF渲染库 import * as pdfjsLib from 'pdfjs-dist'; // 配置Worker pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.js'; async function renderPDF(url, container) { // 加载PDF文档 const loadingTask = pdfjsLib.getDocument(url); const pdf = await loadingTask.promise; console.log(`PDF加载完成,共${pdf.numPages}页`); // 渲染第一页 const page = await pdf.getPage(1); const viewport = page.getViewport({ scale: 1.5 }); // 创建canvas const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; // 渲染 await page.render({ canvasContext: context, viewport: viewport, }).promise; container.appendChild(canvas); } // 使用 renderPDF('/sample.pdf', document.getElementById('viewer'));
提取PDF文本
async function extractText(url) { const pdf = await pdfjsLib.getDocument(url).promise; const textContent = []; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); const pageText = content.items.map((item) => item.str).join(' '); textContent.push({ page: i, text: pageText, }); } return textContent; } // 使用 const text = await extractText('/document.pdf'); console.log(text[0].text); // 第一页的文本
PDF缩略图生成
async function generateThumbnail(url, pageNumber = 1, scale = 0.3) { const pdf = await pdfjsLib.getDocument(url).promise; const page = await pdf.getPage(pageNumber); const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: context, viewport: viewport, }).promise; // 转换为Data URL return canvas.toDataURL('image/jpeg', 0.8); } // 生成所有页面的缩略图 async function generateAllThumbnails(url) { const pdf = await pdfjsLib.getDocument(url).promise; const thumbnails = []; for (let i = 1; i <= pdf.numPages; i++) { const thumb = await generateThumbnail(url, i); thumbnails.push(thumb); } return thumbnails; }
常见PDF操作
1. PDF合并
// 使用pdf-lib库 import { PDFDocument } from 'pdf-lib'; async function mergePDFs(pdfFiles) { // 创建新的PDF文档 const mergedPdf = await PDFDocument.create(); for (const file of pdfFiles) { // 读取文件 const arrayBuffer = await file.arrayBuffer(); const pdf = await PDFDocument.load(arrayBuffer); // 复制所有页面 const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); // 添加到新文档 copiedPages.forEach((page) => { mergedPdf.addPage(page); }); } // 保存 const pdfBytes = await mergedPdf.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); } // 使用 document.getElementById('mergeBtn').addEventListener('click', async () => { const files = document.getElementById('fileInput').files; const merged = await mergePDFs(Array.from(files)); // 下载 const url = URL.createObjectURL(merged); const a = document.createElement('a'); a.href = url; a.download = 'merged.pdf'; a.click(); URL.revokeObjectURL(url); });
2. PDF分割
async function splitPDF(file, pageRanges) { const arrayBuffer = await file.arrayBuffer(); const pdf = await PDFDocument.load(arrayBuffer); const result = []; for (const range of pageRanges) { const newPdf = await PDFDocument.create(); const pages = await newPdf.copyPages(pdf, range); pages.forEach((page) => newPdf.addPage(page)); const pdfBytes = await newPdf.save(); result.push(new Blob([pdfBytes], { type: 'application/pdf' })); } return result; } // 使用示例 // 分割为多个单页PDF async function splitIntoPages(file) { const arrayBuffer = await file.arrayBuffer(); const pdf = await PDFDocument.load(arrayBuffer); const pageCount = pdf.getPageCount(); const ranges = Array.from({ length: pageCount }, (_, i) => [i]); return await splitPDF(file, ranges); } // 分割为指定范围 const parts = await splitPDF(file, [ [0, 1, 2], // 第1-3页 [3, 4, 5, 6], // 第4-7页 [7, 8, 9], // 第8-10页 ]);
3. PDF压缩
async function compressPDF(file, quality = 0.7) { const arrayBuffer = await file.arrayBuffer(); const pdf = await PDFDocument.load(arrayBuffer); // 压缩策略:降低图片质量 const pages = pdf.getPages(); for (const page of pages) { // 获取页面资源 const resources = page.node.Resources(); if (resources && resources.lookup(PDFName.of('XObject'))) { const xObjects = resources.lookup(PDFName.of('XObject')); // 遍历所有图片对象 xObjects.entries().forEach(([name, ref]) => { const image = pdf.context.lookup(ref); if (image && image.lookup(PDFName.of('Subtype'))?.toString() === '/Image') { // 这里可以实现图片压缩逻辑 // 注意:pdf-lib的压缩功能有限,复杂压缩需要服务端处理 } }); } } // 保存时使用压缩选项 const pdfBytes = await pdf.save({ useObjectStreams: true, // 使用对象流压缩 addDefaultPage: false, }); return new Blob([pdfBytes], { type: 'application/pdf' }); }
注意:浏览器端PDF压缩能力有限,大幅压缩建议使用服务端工具:
# 使用Ghostscript压缩(服务端) gs -sDEVICE=pdfwrite \ -dCompatibilityLevel=1.4 \ -dPDFSETTINGS=/ebook \ -dNOPAUSE -dQUIET -dBATCH \ -sOutputFile=compressed.pdf \ input.pdf # 压缩质量选项 # /screen - 低质量,小文件(72dpi) # /ebook - 中等质量(150dpi) # /printer - 高质量(300dpi) # /prepress - 印刷质量(300dpi,保留颜色)
4. PDF转图片
async function pdfToImages(file, scale = 2) { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument(arrayBuffer).promise; const images = []; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: context, viewport: viewport, }).promise; // 转换为高质量JPEG const imageData = canvas.toDataURL('image/jpeg', 0.95); images.push({ page: i, data: imageData, width: viewport.width, height: viewport.height, }); } return images; } // 下载所有图片 async function downloadPDFAsImages(file) { const images = await pdfToImages(file); images.forEach((img, index) => { const a = document.createElement('a'); a.href = img.data; a.download = `page-${index + 1}.jpg`; a.click(); }); }
5. 图片转PDF
async function imagesToPDF(imageFiles) { const pdfDoc = await PDFDocument.create(); for (const file of imageFiles) { const arrayBuffer = await file.arrayBuffer(); let image; if (file.type === 'image/jpeg' || file.type === 'image/jpg') { image = await pdfDoc.embedJpg(arrayBuffer); } else if (file.type === 'image/png') { image = await pdfDoc.embedPng(arrayBuffer); } else { console.warn(`跳过不支持的格式: ${file.type}`); continue; } // 创建页面(A4尺寸或图片原始尺寸) const page = pdfDoc.addPage([image.width, image.height]); // 绘制图片 page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height, }); } const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); } // 使用 const images = document.getElementById('imageInput').files; const pdf = await imagesToPDF(Array.from(images));
6. 添加水印
async function addWatermark(file, watermarkText) { const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await PDFDocument.load(arrayBuffer); const pages = pdfDoc.getPages(); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); pages.forEach((page) => { const { width, height } = page.getSize(); // 添加文字水印 page.drawText(watermarkText, { x: width / 2 - 100, y: height / 2, size: 50, font: font, color: rgb(0.7, 0.7, 0.7), rotate: degrees(45), opacity: 0.3, }); }); const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); } // 添加图片水印 async function addImageWatermark(file, watermarkImageUrl) { const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await PDFDocument.load(arrayBuffer); const imageBytes = await fetch(watermarkImageUrl).then((res) => res.arrayBuffer()); const watermark = await pdfDoc.embedPng(imageBytes); const pages = pdfDoc.getPages(); pages.forEach((page) => { const { width, height } = page.getSize(); const watermarkSize = 100; page.drawImage(watermark, { x: width - watermarkSize - 20, y: 20, width: watermarkSize, height: watermarkSize, opacity: 0.5, }); }); const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); }
PDF优化技巧
减小文件大小
1. 压缩图片
async function optimizeImages(pdfFile) { // 提取PDF中的图片 const images = await extractImages(pdfFile); // 压缩每张图片 const compressedImages = await Promise.all(images.map((img) => compressImage(img, 0.7))); // 重新创建PDF return await createPDFFromImages(compressedImages); } async function compressImage(imageBlob, quality) { return new Promise((resolve) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); canvas.toBlob(resolve, 'image/jpeg', quality); }; img.src = URL.createObjectURL(imageBlob); }); }
2. 删除元数据
async function removeMetadata(file) { const arrayBuffer = await file.arrayBuffer(); const pdfDoc = await PDFDocument.load(arrayBuffer); // 清除元数据 pdfDoc.setTitle(''); pdfDoc.setAuthor(''); pdfDoc.setSubject(''); pdfDoc.setKeywords([]); pdfDoc.setProducer(''); pdfDoc.setCreator(''); const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); }
3. 删除未使用的资源
// 这通常需要专业工具 // Adobe Acrobat Pro: 文件 > 另存为其他 > 优化的PDF // Ghostscript: 会自动清理未使用资源
提高加载速度
1. 启用快速Web查看
// pdf-lib默认生成线性化PDF(快速Web查看) const pdfBytes = await pdfDoc.save({ useObjectStreams: false, // 禁用对象流以支持线性化 });
2. 分页加载
class PDFViewer { constructor(url, container) { this.url = url; this.container = container; this.currentPage = 1; } async init() { this.pdf = await pdfjsLib.getDocument(this.url).promise; this.totalPages = this.pdf.numPages; await this.renderPage(1); } async renderPage(pageNumber) { const page = await this.pdf.getPage(pageNumber); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport, }).promise; this.container.innerHTML = ''; this.container.appendChild(canvas); this.currentPage = pageNumber; } nextPage() { if (this.currentPage < this.totalPages) { this.renderPage(this.currentPage + 1); } } prevPage() { if (this.currentPage > 1) { this.renderPage(this.currentPage - 1); } } }
PDF安全性
加密PDF
// 注意:pdf-lib的加密功能有限 // 真正的PDF加密需要使用专业工具或服务端处理 async function encryptPDF(file, password) { // 浏览器端加密PDF较复杂 // 推荐使用服务端API const formData = new FormData(); formData.append('file', file); formData.append('password', password); const response = await fetch('/api/pdf/encrypt', { method: 'POST', body: formData, }); return await response.blob(); } // 服务端示例(Node.js + pdf-lib) // const { PDFDocument, StandardFonts } = require('pdf-lib'); // // async function encryptPDF(inputPath, outputPath, password) { // const pdfBytes = fs.readFileSync(inputPath); // const pdfDoc = await PDFDocument.load(pdfBytes); // // const encryptedPdfBytes = await pdfDoc.save({ // userPassword: password, // ownerPassword: password, // permissions: { // printing: 'highResolution', // modifying: false, // copying: false, // annotating: false, // fillingForms: false, // contentAccessibility: true, // documentAssembly: false // } // }); // // fs.writeFileSync(outputPath, encryptedPdfBytes); // }
数字签名
// PDF数字签名需要证书 // 通常在服务端处理 async function signPDF(file, certificateData) { const formData = new FormData(); formData.append('file', file); formData.append('certificate', certificateData); const response = await fetch('/api/pdf/sign', { method: 'POST', body: formData, }); return await response.blob(); }
权限控制
const permissions = { printing: 'none', // 禁止打印 modifying: false, // 禁止修改 copying: false, // 禁止复制 annotating: false, // 禁止注释 fillingForms: false, // 禁止填写表单 contentAccessibility: true, // 允许屏幕阅读器 documentAssembly: false, // 禁止页面提取 };
实战工作流
工作流1:批量发票处理
class InvoiceProcessor { async process(invoiceFiles) { const results = []; for (const file of invoiceFiles) { try { // 1. 提取文本 const text = await this.extractText(file); // 2. 解析发票信息 const invoiceData = this.parseInvoiceData(text); // 3. 验证数据 if (this.validate(invoiceData)) { // 4. 添加水印 const watermarked = await addWatermark(file, 'PROCESSED'); // 5. 重命名文件 const newName = `${invoiceData.number}_${invoiceData.date}.pdf`; results.push({ success: true, file: watermarked, name: newName, data: invoiceData, }); } } catch (error) { results.push({ success: false, error: error.message, }); } } return results; } parseInvoiceData(text) { // 使用正则提取发票号、日期、金额等 const numberMatch = text.match(/发票号码[::]\s*(\S+)/); const dateMatch = text.match(/日期[::]\s*(\d{4}-\d{2}-\d{2})/); const amountMatch = text.match(/金额[::]\s*¥?(\d+\.?\d*)/); return { number: numberMatch?.[1], date: dateMatch?.[1], amount: amountMatch?.[1], }; } validate(data) { return data.number && data.date && data.amount; } async extractText(file) { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument(arrayBuffer).promise; const page = await pdf.getPage(1); const content = await page.getTextContent(); return content.items.map((item) => item.str).join(' '); } }
工作流2:电子书制作
class EbookMaker { async create(chapters) { const pdfDoc = await PDFDocument.create(); const font = await pdfDoc.embedFont(StandardFonts.TimesRoman); const boldFont = await pdfDoc.embedFont(StandardFonts.TimesRomanBold); let pageNumber = 1; for (const chapter of chapters) { // 添加章节标题页 const titlePage = pdfDoc.addPage([595, 842]); // A4 titlePage.drawText(chapter.title, { x: 50, y: 750, size: 24, font: boldFont, }); // 添加章节内容 const lines = this.splitIntoLines(chapter.content, 70); let yPosition = 700; for (const line of lines) { if (yPosition < 100) { // 新建页面 const page = pdfDoc.addPage([595, 842]); yPosition = 750; // 添加页码 page.drawText(`${pageNumber}`, { x: 550, y: 50, size: 10, font: font, }); pageNumber++; } titlePage.drawText(line, { x: 50, y: yPosition, size: 12, font: font, }); yPosition -= 20; } pageNumber++; } // 添加目录 await this.addTableOfContents(pdfDoc, chapters); const pdfBytes = await pdfDoc.save(); return new Blob([pdfBytes], { type: 'application/pdf' }); } splitIntoLines(text, maxChars) { const words = text.split(' '); const lines = []; let currentLine = ''; for (const word of words) { if ((currentLine + word).length > maxChars) { lines.push(currentLine.trim()); currentLine = word + ' '; } else { currentLine += word + ' '; } } if (currentLine) { lines.push(currentLine.trim()); } return lines; } async addTableOfContents(pdfDoc, chapters) { const tocPage = pdfDoc.insertPage(0, [595, 842]); const font = await pdfDoc.embedFont(StandardFonts.TimesRomanBold); tocPage.drawText('目录', { x: 250, y: 750, size: 20, font: font, }); let yPosition = 700; chapters.forEach((chapter, index) => { tocPage.drawText(`${index + 1}. ${chapter.title}`, { x: 50, y: yPosition, size: 14, font: font, }); yPosition -= 30; }); } }
工作流3:简历筛选系统
class ResumeScreener { constructor(keywords) { this.keywords = keywords; } async screen(resumeFiles) { const results = []; for (const file of resumeFiles) { const text = await this.extractText(file); const score = this.calculateScore(text); const matches = this.findKeywordMatches(text); results.push({ filename: file.name, score: score, matches: matches, qualified: score >= 60, }); } // 按分数排序 return results.sort((a, b) => b.score - a.score); } calculateScore(text) { let score = 0; const lowerText = text.toLowerCase(); this.keywords.forEach((keyword) => { const regex = new RegExp(keyword.term, 'gi'); const matches = lowerText.match(regex); if (matches) { score += keyword.weight * matches.length; } }); return Math.min(score, 100); } findKeywordMatches(text) { const matches = []; this.keywords.forEach((keyword) => { const regex = new RegExp(keyword.term, 'gi'); const found = text.match(regex); if (found) { matches.push({ keyword: keyword.term, count: found.length, }); } }); return matches; } async extractText(file) { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument(arrayBuffer).promise; let fullText = ''; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); fullText += content.items.map((item) => item.str).join(' ') + '\n'; } return fullText; } } // 使用 const screener = new ResumeScreener([ { term: 'javascript', weight: 10 }, { term: 'react', weight: 8 }, { term: 'node\\.js', weight: 8 }, { term: '本科', weight: 5 }, { term: '3年经验', weight: 7 }, ]); const results = await screener.screen(resumeFiles); console.log( '合格简历:', results.filter((r) => r.qualified) );
总结
PDF处理是日常工作中的常见需求,掌握相关技术能大幅提升效率:
核心要点
-
选择合适的库:
- PDF.js:渲染和阅读
- pdf-lib:创建和修改
- jsPDF:从零创建PDF
-
浏览器vs服务端:
- 浏览器:适合轻量级操作
- 服务端:适合复杂处理(加密、OCR、压缩)
-
性能优化:
- 分页加载
- 压缩图片
- 启用快速Web查看
-
安全考虑:
- 敏感文档加密
- 权限控制
- 数字签名验证
最佳实践
- ✅ 大文件使用服务端处理
- ✅ 压缩前备份原始文件
- ✅ 合并前检查文件完整性
- ✅ 使用Web Worker避免阻塞UI
- ✅ 添加错误处理和进度提示
相关工具
关键词: PDF处理, PDF合并, PDF分割, PDF压缩, PDF.js, pdf-lib
更新时间: 2026-01-05
相关阅读
其他
TypeScript 类型体操:什么时候值,什么时候在炫技
务实的 TypeScript 高级类型指南 — mapped types、conditional types、template literal types 真正能给你什么,什么时候用,什么时候应该退回到朴素代码。
2026-05-21
其他
Kubernetes 资源 requests / limits 实战:不会把生产搞挂的设法
怎么在生产里实际设 Kubernetes CPU 与内存的 requests/limits — QoS 类、CPU 节流、OOM kill、那些害公司钱的差别,以及好使的模式。
2026-05-18
其他
Vue 3 vs React 2026:下个项目的诚实对比
2026 年 Vue 3 与 React 的诚实对比 — Composition API vs Hooks、性能、生态、TypeScript 表现,以及真正决定选型的标准。
2026-05-15