Eigent PDF 技能进阶参考:pypdfium2、pdf-lib、poppler-utils 与 qpdf 的实战指南
Eigent PDF 技能进阶参考pypdfium2、pdf-lib、poppler-utils 与 qpdf 的实战指南【免费下载链接】eigentEigent: The Open Source Cowork Desktop - Local and Free Alternative to Claude Cowork and Codex项目地址: https://gitcode.com/GitHub_Trending/ei/eigent本文基于 Eigent 仓库内置 PDF 示例技能的进阶参考文档 resources/example-skills/pdf/reference.md 展开系统讲解 Python 与 JavaScript 两侧的 PDF 处理库、poppler-utils 与 qpdf 命令行的高级用法、复杂工作流与性能优化策略。读完本文你既能直接在 Eigent 的 Agent 技能体系中复用这套 PDF 处理能力也能将其中所有代码示例与命令复制到自己的项目中独立运行。PDF 技能在 Eigent 中的定位Eigent 在resources/example-skills/目录下内置了一组开箱即用的示例技能docx、pdf、pptx、xlsx、skill-creator 等PDF 技能由三份文档与一组脚本组成SKILL.md主指南覆盖 pypdf 合并/拆分/加密、pdfplumber 文本与表格提取、reportlab 创建 PDF以及pdftotext、qpdf、pdftk基础命令reference.md本文的主体——进阶特性、JavaScript 库、命令行高级用法、复杂工作流、性能优化与故障排查forms.mdPDF 表单填写的完整决策流程先检测可填写字段再分别走 fillable / non-fillable 两条路径resources/example-skills/pdf/scripts/下的辅助脚本供 Agent 在工作目录中直接调用。从源码结构看这些技能在开发态与打包态有不同解析路径skill_service.py 中的_candidate_example_skill_roots()会依次尝试环境变量、Resources/example-skills打包应用、repo/resources/example-skills开发仓库等候选目录electron-builder.json 则负责在构建时把resources/example-skills拷贝进应用的资源目录。因此下文中所有脚本与文档的用法在仓库内与安装后的桌面端中均可按同一方式工作。pypdfium2基于 PDFium 的快速渲染与文本提取pypdfium2 是 PDFiumChromium 的 PDF 引擎的 Python 绑定许可证为 Apache/BSD。它的核心价值在于快速渲染页面为图像可作为 PyMuPDF 的替代方案特别适合批量把 PDF 页面转成 PNG/JPEG 或生成高分辨率预览。将 PDF 渲染为图片import pypdfium2 as pdfium from PIL import Image # Load PDF pdf pdfium.PdfDocument(document.pdf) # Render page to image page pdf[0] # First page bitmap page.render( scale2.0, # Higher resolution rotation0 # No rotation ) # Convert to PIL Image img bitmap.to_pil() img.save(page_1.png, PNG) # Process multiple pages for i, page in enumerate(pdf): bitmap page.render(scale1.5) img bitmap.to_pil() img.save(fpage_{i1}.jpg, JPEG, quality90)关键点render(scale...)控制渲染分辨率scale2.0意味着以两倍基准分辨率光栅化to_pil()将位图转为 PIL 图像后即可用Pillow的完整生态裁剪、拼接、压缩。用 pypdfium2 提取文本import pypdfium2 as pdfium pdf pdfium.PdfDocument(document.pdf) for i, page in enumerate(pdf): text page.get_text() print(fPage {i1} text length: {len(text)} chars)逐页迭代PdfDocument即可拿到每页文本这种逐页处理模式也是后文“大文件分块处理”的基础。JavaScript 库一pdf-libMIT 许可pdf-lib 可在任意 JavaScript 环境中创建与修改 PDF特点是对表单结构的保持能力优于大多数同类库这一点在性能优化一节还有呼应。加载并修改已有 PDFimport { PDFDocument } from pdf-lib; import fs from fs; async function manipulatePDF() { // Load existing PDF const existingPdfBytes fs.readFileSync(input.pdf); const pdfDoc await PDFDocument.load(existingPdfBytes); // Get page count const pageCount pdfDoc.getPageCount(); console.log(Document has ${pageCount} pages); // Add new page const newPage pdfDoc.addPage([600, 400]); newPage.drawText(Added by pdf-lib, { x: 100, y: 300, size: 16 }); // Save modified PDF const pdfBytes await pdfDoc.save(); fs.writeFileSync(modified.pdf, pdfBytes); }从零创建复杂 PDFimport { PDFDocument, rgb, StandardFonts } from pdf-lib; import fs from fs; async function createPDF() { const pdfDoc await PDFDocument.create(); // Add fonts const helveticaFont await pdfDoc.embedFont(StandardFonts.Helvetica); const helveticaBold await pdfDoc.embedFont(StandardFonts.HelveticaBold); // Add page const page pdfDoc.addPage([595, 842]); // A4 size const { width, height } page.getSize(); // Add text with styling page.drawText(Invoice #12345, { x: 50, y: height - 50, size: 18, font: helveticaBold, color: rgb(0.2, 0.2, 0.8) }); // Add rectangle (header background) page.drawRectangle({ x: 40, y: height - 100, width: width - 80, height: 30, color: rgb(0.9, 0.9, 0.9) }); // Add table-like content const items [ [Item, Qty, Price, Total], [Widget, 2, $50, $100], [Gadget, 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); }这里体现了 pdf-lib 的坐标系习惯addPage([width, height])以点point为单位定义页面尺寸[595, 842]即 A4文字与图形都从页面左下角的坐标系描述。高级合并与拆分import { PDFDocument } from pdf-lib; import fs from fs; async function mergePDFs() { // Create new document const mergedPdf await PDFDocument.create(); // Load source PDFs const pdf1Bytes fs.readFileSync(doc1.pdf); const pdf2Bytes fs.readFileSync(doc2.pdf); const pdf1 await PDFDocument.load(pdf1Bytes); const pdf2 await PDFDocument.load(pdf2Bytes); // Copy pages from first PDF const pdf1Pages await mergedPdf.copyPages(pdf1, pdf1.getPageIndices()); pdf1Pages.forEach(page mergedPdf.addPage(page)); // Copy specific pages from second PDF (pages 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); }copyPages(sourceDoc, indices)支持传入任意页索引数组0 基因此“选择性抽取指定页再合并”不需要额外的拆分步骤。JavaScript 库二pdfjs-distApache 许可pdfjs-dist 即 Mozilla 的 PDF.js是在浏览器中渲染 PDF的事实标准同时提供带坐标的文本提取与注释annotation读取能力。基本加载与渲染import * as pdfjsLib from pdfjs-dist; // Configure worker (important for performance) pdfjsLib.GlobalWorkerOptions.workerSrc ./pdf.worker.js; async function renderPDF() { // Load PDF const loadingTask pdfjsLib.getDocument(document.pdf); const pdf await loadingTask.promise; console.log(Loaded PDF with ${pdf.numPages} pages); // Get first page const page await pdf.getPage(1); const viewport page.getViewport({ scale: 1.5 }); // Render to 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); }注意GlobalWorkerOptions.workerSrc的配置PDF.js 将解析与渲染放到 Web Worker 中执行正确指向pdf.worker.js是避免阻塞主线程的关键。带坐标的文本提取import * as pdfjsLib from pdfjs-dist; async function extractText() { const loadingTask pdfjsLib.getDocument(document.pdf); const pdf await loadingTask.promise; let fullText ; // Extract text from all pages 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--- Page ${i} ---\n${pageText}; // Get text with coordinates for advanced processing 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; }每个文本项的transform是 PDF 仿射变换矩阵transform[4]、transform[5]分别对应 x、y 平移分量——把文本映射回页面坐标例如做高亮、做检索定位都依赖这一组数值。提取注释与表单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 type: ${annotation.subtype}); console.log(Content: ${annotation.contents}); console.log(Coordinates: ${JSON.stringify(annotation.rect)}); }); } }annotation.subtype可以区分 Text、Link、Widget表单控件等类型rect给出注释在页面上的包围盒是自动化处理批注类 PDF 的入口。poppler-utils 进阶命令行带边界框坐标的文本提取# Extract text with bounding box coordinates (essential for structured data) pdftotext -bbox-layout document.pdf output.xml # The XML output contains precise coordinates for each text element-bbox-layout输出的是 XML其中每个文本元素都带有精确坐标适合需要还原版面结构结构化数据抽取的场景而不仅仅是纯文本。高级图像转换# Convert to PNG images with specific resolution pdftoppm -png -r 300 document.pdf output_prefix # Convert specific page range with high resolution pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages # Convert to JPEG with quality setting pdftoppm -jpeg -jpegopt quality85 -r 200 document.pdf jpeg_output参数含义-r指定 DPI每英寸点数-f/-l限定页码范围1 基-jpegopt quality85控制 JPEG 质量。Eigent 仓库中的 convert_pdf_to_images.py 正是这条路线的 Python 实现它用pdf2image.convert_from_path(pdf_path, dpi200)以 200 DPI 渲染再对超过max_dim1000像素的图像等比缩小最后存为page_{i1}.png——“先高分辨率转换、再降采样控制体积”的思路与上面的命令行参数完全一致。提取内嵌图像# Extract all embedded images with metadata pdfimages -j -p document.pdf page_images # List image info without extracting pdfimages -list document.pdf # Extract images in their original format pdfimages -all document.pdf images/img-list只列出图像清单尺寸、色彩空间等元数据不落盘-all以原始编码格式提取速度远快于整页渲染后再裁图。qpdf 进阶页面操作、优化修复与加密复杂页面操作# Split PDF into groups of pages qpdf --split-pages3 input.pdf output_group_%02d.pdf # Extract specific pages with complex ranges qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf # Merge specific pages from multiple PDFs qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf页范围语法支持混用单页8、区间3-5、开区间10-end一条命令即可表达“从多个 PDF 中各取部分页再合并”的复杂编排。PDF 优化与修复# Optimize for web (linearize for streaming) qpdf --linearize input.pdf optimized.pdf # Remove unused objects and compress qpdf --optimize-levelall input.pdf compressed.pdf # Attempt to repair corrupted PDF structure qpdf --check input.pdf qpdf --fix-qdf damaged.pdf repaired.pdf # Show detailed PDF structure for debugging qpdf --show-all-pages input.pdf structure.txt--linearize生成“快速查看”格式让浏览器可以边下载边渲染首页--optimize-levelall会去重对象并压缩流--check用于诊断结构损坏--fix-qdf尝试重建损坏的 QDF 结构。高级加密# Add password protection with specific permissions qpdf --encrypt user_pass owner_pass 256 --printnone --modifynone -- input.pdf encrypted.pdf # Check encryption status qpdf --show-encryption encrypted.pdf # Remove password protection (requires password) qpdf --passwordsecret123 --decrypt encrypted.pdf decrypted.pdf--encrypt user_pass owner_pass 256中第三个参数是密钥长度256 位 AES--printnone --modifynone收紧用户密码对应的权限。对照 SKILL.md 中 Python 侧的writer.encrypt(userpassword, ownerpassword)两条路径命令行 / pypdf分别覆盖了不同技术栈下的加密需求。pdfplumber 进阶坐标与表格精确坐标的字符级提取import pdfplumber with pdfplumber.open(document.pdf) as pdf: page pdf.pages[0] # Extract all text with coordinates chars page.chars for char in chars[:10]: # First 10 characters print(fChar: {char[text]} at x:{char[x0]:.1f} y:{char[y0]:.1f}) # Extract text by bounding box (left, top, right, bottom) bbox_text page.within_bbox((100, 100, 400, 200)).extract_text()page.chars提供每个字符的x0/y0等包围盒属性within_bbox((left, top, right, bottom))则允许你只抽取页面某个矩形区域内的文本——例如只取侧边栏或某个表单区块。自定义表格提取设置import pdfplumber import pandas as pd with pdfplumber.open(complex_table.pdf) as pdf: page pdf.pages[0] # Extract tables with custom settings for complex layouts table_settings { vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, intersection_tolerance: 15 } tables page.extract_tables(table_settings) # Visual debugging for table extraction img page.to_image(resolution150) img.save(debug_layout.png)vertical_strategy/horizontal_strategy决定表格线检测策略lines依赖实际线条text依赖字符对齐snap_tolerance与intersection_tolerance控制线条吸附与交叉点判定的容差。当提取结果不理想时page.to_image(resolution150)渲染调试图是定位版面问题的常用手段。reportlab 进阶带表格的专业报告from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib import colors # Sample data data [ [Product, Q1, Q2, Q3, Q4], [Widgets, 120, 135, 142, 158], [Gadgets, 85, 92, 98, 105] ] # Create PDF with table doc SimpleDocTemplate(report.pdf) elements [] # Add title styles getSampleStyleSheet() title Paragraph(Quarterly Sales Report, styles[Title]) elements.append(title) # Add table with advanced styling 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)TableStyle的每条指令形如(指令, (起始列, 起始行), (结束列, 结束行), 参数...)-1表示“最后一列/行”。配合 SKILL.md 中关于上下标的重要提示禁止在 ReportLab PDF 中使用 Unicode 上下标字符应改用Paragraph的sub/super标签即可覆盖报告类文档的大部分排版需求。复杂工作流从 PDF 中提取插图方法一最快直接用 poppler 的pdfimages# Extract all images with original quality pdfimages -all document.pdf images/img方法二用 pypdfium2 渲染高分辨率页面后做图像处理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): # Render high-resolution page bitmap page.render(scale3.0) img bitmap.to_pil() # Convert to numpy for processing img_array np.array(img) # Simple figure detection (non-white regions) mask np.any(img_array ! [255, 255, 255], axis2) # Find contours and extract bounding boxes # (This is simplified - real implementation would need more sophisticated detection) # Save detected figures # ... implementation depends on specific needs原文档也明确说明基于非白区域掩码的图形检测是简化实现生产环境需要更完善的轮廓检测如 OpenCV 连通域分析此处给出的是可扩展的骨架。带错误处理的批量 PDF 处理import os import glob from pypdf import PdfReader, PdfWriter import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def batch_process_pdfs(input_dir, operationmerge): 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(fProcessed: {pdf_file}) except Exception as e: logger.error(fFailed to process {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, encodingutf-8) as f: f.write(text) logger.info(fExtracted text from: {pdf_file}) except Exception as e: logger.error(fFailed to extract text from {pdf_file}: {e}) continue批处理的两个要点单文件失败用try/except continue隔离不让一个坏文件毁掉整批任务merge与extract_text两种操作共用同一套文件发现与日志机制便于扩展新的operation。高级裁剪from pypdf import PdfWriter, PdfReader reader PdfReader(input.pdf) writer PdfWriter() # Crop page (left, bottom, right, top in points) 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)注意裁剪参数按 PDF 坐标系原点在左下角给出mediabox的left/bottom/right/top四边各自以点为单位调整等价于重新定义页面可见区域。性能优化建议原文档给出的五条优化原则与一个分块处理示例完整继承如下大 PDF优先流式处理而非整体载入内存拆分大文件用qpdf --split-pages用 pypdfium2 逐页处理。文本提取纯文本提取用pdftotext -bbox-layout最快结构化数据与表格用 pdfplumber超大文档避免pypdf.extract_text()。图像提取pdfimages远快于整页渲染预览用低分辨率、最终产物用高分辨率。表单填写pdf-lib 对表单结构的保持优于大多数替代品处理前先预校验表单字段。内存管理分块处理大 PDF。# Process PDFs in chunks def process_large_pdf(pdf_path, chunk_size10): 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]) # Process chunk with open(fchunk_{start_idx//chunk_size}.pdf, wb) as output: writer.write(output)每chunk_size页写一个独立的中间 PDF峰值内存只与单块页数相关而非与文档总页数相关。常见问题排查加密 PDF# Handle password-protected PDFs from pypdf import PdfReader try: reader PdfReader(encrypted.pdf) if reader.is_encrypted: reader.decrypt(password) except Exception as e: print(fFailed to decrypt: {e})损坏的 PDF# Use qpdf to repair qpdf --check corrupted.pdf qpdf --replace-input corrupted.pdf先用--check诊断损坏位置再让--replace-input就地重建。扫描件文本提取OCR 兜底# Fallback to OCR for scanned PDFs 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当常规提取拿不到文本扫描件没有文本层时将每页转成图像再交给pytesseract识别是标准兜底路径依赖pytesseract与系统安装的 Tesseract。源码纵深技能辅助脚本背后的实现细节reference.md 的表单相关流程依赖 forms.md 中定义的“先检测、再分路”策略而真正落地的验证与填写逻辑在配套脚本中值得结合源码看两处关键实现填写字段前的强制校验。fill_fillable_fields.py 在写回任何值之前会调用 extract_form_field_info.py 的get_field_info()重新解析 PDF 的真实字段表并逐项核对field_id是否存在、page是否一致、checkbox/radio/choice 的取值是否在合法集合内validation_error_for_field_value会列出该字段的合法值。任何一项不符即打印ERROR并sys.exit(1)。校验通过后才执行writer PdfWriter(clone_fromreader) for page, field_values in fields_by_page.items(): writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerateFalse) writer.set_need_appearances_writer(True)其中auto_regenerateFalse保留原字段的显示外观设置set_need_appearances_writer(True)则把“字段外观需要重绘”的标志位写回 PDF交由阅读器在打开时重建外观——两者配合是 pypdf 填写表单后仍能正常显示的标准做法。字段识别的底层依据。extract_form_field_info.py 通过 PDF 对象字典识别字段get_full_annotation_field_id()沿注释的/Parent链拼接/T得到完整字段名make_field_dict()依据/FT键区分类型/Tx→ text/Btn→ checkbox/Ch→ choicecheckbox 再从/States_中解析出checked_value与unchecked_value。脚本还专门处理了 radio 组带/Kids且/FT /Btn的字段被视为候选 radio 组逐页扫描注释的/AP/N收集各选项的on value与rect最终按“页码 页面自上而下位置”排序输出——这正是 forms.md 中 JSON 结构里radio_options数组的来源。此外 fill_fillable_fields.py 还包含一个对 pypdfDictionaryObject.get_inherited的 monkey patch用于把/Optchoice 字段的[value, display]对列表归一化为纯值列表规避了上游库在该键上的类型差异。这些实现细节解释了 reference.md 中“处理前先预校验表单字段”这一建议的工程理由字段类型、合法取值、页码归属全部来自对 PDF 对象结构的直接解析而非猜测。许可证一览原文档结尾汇总了涉及库的许可证完整继承如下合规使用时请注意 GPL-2 的 poppler-utils 以系统包形式分发通常不直接链接进自有代码库许可证pypdfBSDpdfplumberMITpypdfium2Apache/BSDreportlabBSDpoppler-utilsGPL-2qpdfApachepdf-libMITpdfjs-distApache小结reference.md 的价值在于给出了一条“按任务选工具”的清晰映射渲染与图像走 pypdfium2 或pdftoppm浏览器端走 pdfjs-dist创建与结构修改走 reportlab 或 pdf-lib结构重排、优化与加密走 qpdf版面结构化提取走pdftotext -bbox-layout/ pdfplumber。配合 SKILL.md 的基础操作与 forms.md 的表单流程以及在resources/example-skills/pdf/scripts/中可直接运行的校验与填写脚本构成了一套在 Eigent 技能体系内开箱即用、也可独立移植到任何 Python/Node 项目中的 PDF 处理工具箱。【免费下载链接】eigentEigent: The Open Source Cowork Desktop - Local and Free Alternative to Claude Cowork and Codex项目地址: https://gitcode.com/GitHub_Trending/ei/eigent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考