# PDF 处理高级参考 本文档包含主要技能说明中未涵盖的高级 PDF 处理功能、详细示例和附加库。 ## pypdfium2 库(Apache/BSD 许可证) ### 概述 pypdfium2 是 PDFium(Chromium 的 PDF 库)的 Python 绑定。它非常适合快速 PDF 渲染、图片生成,并且可以作为 PyMuPDF 的替代方案。 ### 将 PDF 渲染为图片 ```python import pypdfium2 as pdfium from PIL import Image # 加载 PDF pdf = pdfium.PdfDocument("document.pdf") # 将页面渲染为图片 page = pdf[0] # 第一页 bitmap = page.render( scale=2.0, # 更高分辨率 rotation=0 # 不旋转 ) # 转换为 PIL Image img = bitmap.to_pil() img.save("page_1.png", "PNG") # 处理多个页面 for i, page in enumerate(pdf): bitmap = page.render(scale=1.5) img = bitmap.to_pil() img.save(f"page_{i+1}.jpg", "JPEG", quality=90) ``` ### 使用 pypdfium2 提取文本 ```python import pypdfium2 as pdfium pdf = pdfium.PdfDocument("document.pdf") for i, page in enumerate(pdf): text = page.get_text() print(f"第 {i+1} 页文本长度:{len(text)} 个字符") ``` ## JavaScript 库 ### pdf-lib(MIT 许可证) pdf-lib 是一个功能强大的 JavaScript 库,用于在任何 JavaScript 环境中创建和修改 PDF 文档。 #### 加载和操作现有 PDF ```javascript import { PDFDocument } from 'pdf-lib'; import fs from 'fs'; async function manipulatePDF() { // 加载现有 PDF const existingPdfBytes = fs.readFileSync('input.pdf'); const pdfDoc = await PDFDocument.load(existingPdfBytes); // 获取页数 const pageCount = pdfDoc.getPageCount(); console.log(`文档共有 ${pageCount} 页`); // 添加新页面 const newPage = pdfDoc.addPage([600, 400]); newPage.drawText('由 pdf-lib 添加', { x: 100, y: 300, size: 16 }); // 保存修改后的 PDF const pdfBytes = await pdfDoc.save(); fs.writeFileSync('modified.pdf', pdfBytes); } ``` #### 从零开始创建复杂 PDF ```javascript import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; import fs from 'fs'; async function createPDF() { const pdfDoc = await PDFDocument.create(); // 添加字体 const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); // 添加页面 const page = pdfDoc.addPage([595, 842]); // A4 尺寸 const { width, height } = page.getSize(); // 添加带样式的文本 page.drawText('发票 #12345', { x: 50, y: height - 50, size: 18, font: helveticaBold, color: rgb(0.2, 0.2, 0.8) }); // 添加矩形(页眉背景) page.drawRectangle({ x: 40, y: height - 100, width: width - 80, height: 30, color: rgb(0.9, 0.9, 0.9) }); // 添加表格类内容 const items = [ ['项目', '数量', '单价', '合计'], ['小工具', '2', '$50', '$100'], ['小装置', '1', '$75', '$75'] ]; let yPos = height - 150; items.forEach(row => { let xPos = 50; row.forEach(cell => { page.drawText(cell, { x: xPos, y: yPos, size: 12, font: helveticaFont }); xPos += 120; }); yPos -= 25; }); const pdfBytes = await pdfDoc.save(); fs.writeFileSync('created.pdf', pdfBytes); } ``` #### 高级合并与拆分操作 ```javascript import { PDFDocument } from 'pdf-lib'; import fs from 'fs'; async function mergePDFs() { // 创建新文档 const mergedPdf = await PDFDocument.create(); // 加载源 PDF const pdf1Bytes = fs.readFileSync('doc1.pdf'); const pdf2Bytes = fs.readFileSync('doc2.pdf'); const pdf1 = await PDFDocument.load(pdf1Bytes); const pdf2 = await PDFDocument.load(pdf2Bytes); // 从第一个 PDF 复制页面 const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices()); pdf1Pages.forEach(page => mergedPdf.addPage(page)); // 从第二个 PDF 复制特定页面(第 0、2、4 页) const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]); pdf2Pages.forEach(page => mergedPdf.addPage(page)); const mergedPdfBytes = await mergedPdf.save(); fs.writeFileSync('merged.pdf', mergedPdfBytes); } ``` ### pdfjs-dist(Apache 许可证) PDF.js 是 Mozilla 的 JavaScript 库,用于在浏览器中渲染 PDF。 #### 基本的 PDF 加载与渲染 ```javascript import * as pdfjsLib from 'pdfjs-dist'; // 配置 worker(对性能很重要) pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js'; async function renderPDF() { // 加载 PDF const loadingTask = pdfjsLib.getDocument('document.pdf'); 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.height = viewport.height; canvas.width = viewport.width; const renderContext = { canvasContext: context, viewport: viewport }; await page.render(renderContext).promise; document.body.appendChild(canvas); } ``` #### 提取带坐标的文本 ```javascript import * as pdfjsLib from 'pdfjs-dist'; async function extractText() { const loadingTask = pdfjsLib.getDocument('document.pdf'); const pdf = await loadingTask.promise; let fullText = ''; // 从所有页面提取文本 for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const textContent = await page.getTextContent(); const pageText = textContent.items .map(item => item.str) .join(' '); fullText += `\n--- 第 ${i} 页 ---\n${pageText}`; // 获取带坐标的文本以供高级处理 const textWithCoords = textContent.items.map(item => ({ text: item.str, x: item.transform[4], y: item.transform[5], width: item.width, height: item.height })); } console.log(fullText); return fullText; } ``` #### 提取注释和表单 ```javascript import * as pdfjsLib from 'pdfjs-dist'; async function extractAnnotations() { const loadingTask = pdfjsLib.getDocument('annotated.pdf'); const pdf = await loadingTask.promise; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const annotations = await page.getAnnotations(); annotations.forEach(annotation => { console.log(`注释类型:${annotation.subtype}`); console.log(`内容:${annotation.contents}`); console.log(`坐标:${JSON.stringify(annotation.rect)}`); }); } } ``` ## 高级命令行操作 ### poppler-utils 高级功能 #### 提取带边界框坐标的文本 ```bash # 提取带边界框坐标的文本(对结构化数据至关重要) pdftotext -bbox-layout document.pdf output.xml # XML 输出中包含每个文本元素的精确坐标 ``` #### 高级图片转换 ```bash # 转换为指定分辨率的 PNG 图片 pdftoppm -png -r 300 document.pdf output_prefix # 以高分辨率转换指定页面范围 pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages # 转换为 JPEG 并设置质量 pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output ``` #### 提取嵌入图片 ```bash # 提取所有嵌入图片及元数据 pdfimages -j -p document.pdf page_images # 列出图片信息(不提取) pdfimages -list document.pdf # 以原始格式提取图片 pdfimages -all document.pdf images/img ``` ### qpdf 高级功能 #### 复杂页面操作 ```bash # 将 PDF 拆分为多组页面 qpdf --split-pages=3 input.pdf output_group_%02d.pdf # 使用复杂页码范围提取特定页面 qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf # 合并来自多个 PDF 的特定页面 qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf ``` #### PDF 优化与修复 ```bash # 优化 PDF 以便网络传输(线性化以实现流式传输) qpdf --linearize input.pdf optimized.pdf # 移除未使用对象并压缩 qpdf --optimize-level=all input.pdf compressed.pdf # 尝试修复损坏的 PDF 结构 qpdf --check input.pdf qpdf --fix-qdf damaged.pdf repaired.pdf # 显示详细 PDF 结构以进行调试 qpdf --show-all-pages input.pdf > structure.txt ``` #### 高级加密 ```bash # 添加带特定权限的密码保护 qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf # 检查加密状态 qpdf --show-encryption encrypted.pdf # 移除密码保护(需要密码) qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf ``` ## 高级 Python 技术 ### pdfplumber 高级功能 #### 提取带精确坐标的文本 ```python import pdfplumber with pdfplumber.open("document.pdf") as pdf: page = pdf.pages[0] # 提取所有文本及坐标 chars = page.chars for char in chars[:10]: # 前 10 个字符 print(f"字符:'{char['text']}' 位置 x:{char['x0']:.1f} y:{char['y0']:.1f}") # 按边界框提取文本(左、上、右、下) bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text() ``` #### 自定义设置的高级表格提取 ```python import pdfplumber import pandas as pd with pdfplumber.open("complex_table.pdf") as pdf: page = pdf.pages[0] # 使用自定义设置提取复杂布局的表格 table_settings = { "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, "intersection_tolerance": 15 } tables = page.extract_tables(table_settings) # 表格提取的可视化调试 img = page.to_image(resolution=150) img.save("debug_layout.png") ``` ### reportlab 高级功能 #### 创建带表格的专业报告 ```python from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib import colors # 示例数据 data = [ ['产品', 'Q1', 'Q2', 'Q3', 'Q4'], ['小工具', '120', '135', '142', '158'], ['小装置', '85', '92', '98', '105'] ] # 创建带表格的 PDF doc = SimpleDocTemplate("report.pdf") elements = [] # 添加标题 styles = getSampleStyleSheet() title = Paragraph("季度销售报告", styles['Title']) elements.append(title) # 添加带高级样式的表格 table = Table(data) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.grey), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 14), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black) ])) elements.append(table) doc.build(elements) ``` ## 复杂工作流 ### 从 PDF 提取图形/图片 #### 方法一:使用 pdfimages(最快) ```bash # 以原始质量提取所有图片 pdfimages -all document.pdf images/img ``` #### 方法二:使用 pypdfium2 + 图片处理 ```python import pypdfium2 as pdfium from PIL import Image import numpy as np def extract_figures(pdf_path, output_dir): pdf = pdfium.PdfDocument(pdf_path) for page_num, page in enumerate(pdf): # 渲染高分辨率页面 bitmap = page.render(scale=3.0) img = bitmap.to_pil() # 转换为 numpy 以进行处理 img_array = np.array(img) # 简单的图形检测(非白色区域) mask = np.any(img_array != [255, 255, 255], axis=2) # 查找轮廓并提取边界框 # (此处已简化——实际实现需要更复杂的检测) # 保存检测到的图形 # ……具体实现取决于实际需求 ``` ### 带错误处理的批量 PDF 处理 ```python import os import glob from pypdf import PdfReader, PdfWriter import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def batch_process_pdfs(input_dir, operation='merge'): pdf_files = glob.glob(os.path.join(input_dir, "*.pdf")) if operation == 'merge': writer = PdfWriter() for pdf_file in pdf_files: try: reader = PdfReader(pdf_file) for page in reader.pages: writer.add_page(page) logger.info(f"已处理:{pdf_file}") except Exception as e: logger.error(f"处理 {pdf_file} 失败:{e}") continue with open("batch_merged.pdf", "wb") as output: writer.write(output) elif operation == 'extract_text': for pdf_file in pdf_files: try: reader = PdfReader(pdf_file) text = "" for page in reader.pages: text += page.extract_text() output_file = pdf_file.replace('.pdf', '.txt') with open(output_file, 'w', encoding='utf-8') as f: f.write(text) logger.info(f"已从以下文件提取文本:{pdf_file}") except Exception as e: logger.error(f"从 {pdf_file} 提取文本失败:{e}") continue ``` ### 高级 PDF 裁剪 ```python from pypdf import PdfWriter, PdfReader reader = PdfReader("input.pdf") writer = PdfWriter() # 裁剪页面(左、下、右、上,单位:点) page = reader.pages[0] page.mediabox.left = 50 page.mediabox.bottom = 50 page.mediabox.right = 550 page.mediabox.top = 750 writer.add_page(page) with open("cropped.pdf", "wb") as output: writer.write(output) ``` ## 性能优化技巧 ### 1. 处理大型 PDF - 使用流式方法而非将整个 PDF 加载到内存 - 使用 `qpdf --split-pages` 拆分大文件 - 使用 pypdfium2 逐页处理 ### 2. 文本提取 - `pdftotext -bbox-layout` 是纯文本提取最快的方案 - 使用 pdfplumber 处理结构化数据和表格 - 对于非常大的文档,避免使用 `pypdf.extract_text()` ### 3. 图片提取 - `pdfimages` 比渲染页面快得多 - 预览使用低分辨率,最终输出使用高分辨率 ### 4. 表单填写 - pdf-lib 比大多数替代方案更好地保留表单结构 - 处理前预先验证表单字段 ### 5. 内存管理 ```python # 分块处理 PDF def process_large_pdf(pdf_path, chunk_size=10): reader = PdfReader(pdf_path) total_pages = len(reader.pages) for start_idx in range(0, total_pages, chunk_size): end_idx = min(start_idx + chunk_size, total_pages) writer = PdfWriter() for i in range(start_idx, end_idx): writer.add_page(reader.pages[i]) # 处理块 with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output: writer.write(output) ``` ## 常见问题排查 ### 加密的 PDF ```python # 处理密码保护的 PDF from pypdf import PdfReader try: reader = PdfReader("encrypted.pdf") if reader.is_encrypted: reader.decrypt("password") except Exception as e: print(f"解密失败:{e}") ``` ### 损坏的 PDF ```bash # 使用 qpdf 修复 qpdf --check corrupted.pdf qpdf --replace-input corrupted.pdf ``` ### 文本提取问题 ```python # 对扫描件回退到 OCR import pytesseract from pdf2image import convert_from_path def extract_text_with_ocr(pdf_path): images = convert_from_path(pdf_path) text = "" for i, image in enumerate(images): text += pytesseract.image_to_string(image) return text ``` ## 许可证信息 - **pypdf**:BSD 许可证 - **pdfplumber**:MIT 许可证 - **pypdfium2**:Apache/BSD 许可证 - **reportlab**:BSD 许可证 - **poppler-utils**:GPL-2 许可证 - **qpdf**:Apache 许可证 - **pdf-lib**:MIT 许可证 - **pdfjs-dist**:Apache 许可证