mercury-agent 0.5.7 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +29 -3
  2. package/container/Dockerfile +9 -5
  3. package/container/Dockerfile.base +2 -2
  4. package/container/build.sh +7 -38
  5. package/docs/authoring-profiles.md +2 -2
  6. package/docs/container-lifecycle.md +7 -11
  7. package/docs/memory.md +0 -6
  8. package/docs/prd-config-load.md +1 -1
  9. package/examples/extensions/napkin/index.ts +17 -10
  10. package/examples/extensions/napkin/prompts/kb-distillation.md +6 -6
  11. package/examples/extensions/pdf/skill/SKILL.md +545 -178
  12. package/examples/extensions/pdf/skill/forms.md +273 -214
  13. package/examples/extensions/pdf/skill/reference.md +450 -480
  14. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +221 -58
  15. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +90 -5
  16. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +138 -22
  17. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +191 -27
  18. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +149 -104
  19. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +215 -91
  20. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +130 -86
  21. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +204 -87
  22. package/examples/extensions/tradestation/skill/SKILL.md +2 -2
  23. package/package.json +17 -1
  24. package/resources/templates/env.template +0 -3
  25. package/src/adapters/whatsapp-media.ts +5 -2
  26. package/src/agent/container-entry.ts +2 -2
  27. package/src/agent/container-runner.ts +19 -14
  28. package/src/bridges/telegram.ts +5 -2
  29. package/src/chat-shim.ts +26 -17
  30. package/src/core/media.ts +5 -2
  31. package/src/dashboard/tokens.css +1 -1
  32. package/src/extensions/image-builder.ts +2 -2
  33. package/src/main.ts +2 -2
  34. package/examples/extensions/pdf/skill/LICENSE.txt +0 -30
@@ -1,612 +1,582 @@
1
- # PDF Processing Advanced Reference
1
+ # PDF Advanced Reference
2
2
 
3
- This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions.
3
+ Extended reference for less common operations, advanced techniques, and CLI tool details.
4
4
 
5
- ## pypdfium2 Library (Apache/BSD License)
5
+ ## pypdf -- Advanced Usage
6
6
 
7
- ### Overview
8
- pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement.
7
+ ### Cloning with modifications
9
8
 
10
- ### Render PDF to Images
11
9
  ```python
12
- import pypdfium2 as pdfium
13
- from PIL import Image
14
-
15
- # Load PDF
16
- pdf = pdfium.PdfDocument("document.pdf")
17
-
18
- # Render page to image
19
- page = pdf[0] # First page
20
- bitmap = page.render(
21
- scale=2.0, # Higher resolution
22
- rotation=0 # No rotation
23
- )
24
-
25
- # Convert to PIL Image
26
- img = bitmap.to_pil()
27
- img.save("page_1.png", "PNG")
28
-
29
- # Process multiple pages
30
- for i, page in enumerate(pdf):
31
- bitmap = page.render(scale=1.5)
32
- img = bitmap.to_pil()
33
- img.save(f"page_{i+1}.jpg", "JPEG", quality=90)
10
+ from pypdf import PdfReader, PdfWriter
11
+
12
+ reader = PdfReader("input.pdf")
13
+ writer = PdfWriter()
14
+
15
+ # Clone all pages
16
+ writer.clone_document_from_reader(reader)
17
+
18
+ # Modify metadata
19
+ writer.add_metadata({
20
+ "/Author": "Mercury Agent",
21
+ "/Title": "Processed Document",
22
+ "/Subject": "Automated processing",
23
+ })
24
+
25
+ writer.write("outbox/output.pdf")
26
+ writer.close()
34
27
  ```
35
28
 
36
- ### Extract Text with pypdfium2
29
+ ### Cropping pages
30
+
37
31
  ```python
38
- import pypdfium2 as pdfium
32
+ from pypdf import PdfReader, PdfWriter
33
+ from pypdf.generic import RectangleObject
39
34
 
40
- pdf = pdfium.PdfDocument("document.pdf")
41
- for i, page in enumerate(pdf):
42
- text = page.get_text()
43
- print(f"Page {i+1} text length: {len(text)} chars")
35
+ reader = PdfReader("input.pdf")
36
+ writer = PdfWriter()
37
+
38
+ page = reader.pages[0]
39
+
40
+ # Crop to a specific region (in points, from bottom-left)
41
+ # RectangleObject(left, bottom, right, top)
42
+ page.cropbox = RectangleObject([72, 72, 400, 700])
43
+
44
+ writer.add_page(page)
45
+ writer.write("outbox/cropped.pdf")
46
+ writer.close()
44
47
  ```
45
48
 
46
- ## JavaScript Libraries
49
+ ### Extracting links and annotations
47
50
 
48
- ### pdf-lib (MIT License)
51
+ ```python
52
+ from pypdf import PdfReader
49
53
 
50
- pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
54
+ reader = PdfReader("input.pdf")
55
+ for page in reader.pages:
56
+ if "/Annots" in page:
57
+ for annot in page["/Annots"]:
58
+ obj = annot.get_object()
59
+ if obj.get("/Subtype") == "/Link":
60
+ action = obj.get("/A")
61
+ if action and "/URI" in action:
62
+ print(f"URL: {action['/URI']}")
63
+ ```
51
64
 
52
- #### Load and Manipulate Existing PDF
53
- ```javascript
54
- import { PDFDocument } from 'pdf-lib';
55
- import fs from 'fs';
65
+ ### Removing pages
56
66
 
57
- async function manipulatePDF() {
58
- // Load existing PDF
59
- const existingPdfBytes = fs.readFileSync('input.pdf');
60
- const pdfDoc = await PDFDocument.load(existingPdfBytes);
67
+ ```python
68
+ from pypdf import PdfReader, PdfWriter
61
69
 
62
- // Get page count
63
- const pageCount = pdfDoc.getPageCount();
64
- console.log(`Document has ${pageCount} pages`);
70
+ reader = PdfReader("input.pdf")
71
+ writer = PdfWriter()
65
72
 
66
- // Add new page
67
- const newPage = pdfDoc.addPage([600, 400]);
68
- newPage.drawText('Added by pdf-lib', {
69
- x: 100,
70
- y: 300,
71
- size: 16
72
- });
73
+ pages_to_remove = {2, 5, 7} # 0-indexed
73
74
 
74
- // Save modified PDF
75
- const pdfBytes = await pdfDoc.save();
76
- fs.writeFileSync('modified.pdf', pdfBytes);
77
- }
78
- ```
75
+ for i, page in enumerate(reader.pages):
76
+ if i not in pages_to_remove:
77
+ writer.add_page(page)
79
78
 
80
- #### Create Complex PDFs from Scratch
81
- ```javascript
82
- import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
83
- import fs from 'fs';
84
-
85
- async function createPDF() {
86
- const pdfDoc = await PDFDocument.create();
87
-
88
- // Add fonts
89
- const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
90
- const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
91
-
92
- // Add page
93
- const page = pdfDoc.addPage([595, 842]); // A4 size
94
- const { width, height } = page.getSize();
95
-
96
- // Add text with styling
97
- page.drawText('Invoice #12345', {
98
- x: 50,
99
- y: height - 50,
100
- size: 18,
101
- font: helveticaBold,
102
- color: rgb(0.2, 0.2, 0.8)
103
- });
104
-
105
- // Add rectangle (header background)
106
- page.drawRectangle({
107
- x: 40,
108
- y: height - 100,
109
- width: width - 80,
110
- height: 30,
111
- color: rgb(0.9, 0.9, 0.9)
112
- });
113
-
114
- // Add table-like content
115
- const items = [
116
- ['Item', 'Qty', 'Price', 'Total'],
117
- ['Widget', '2', '$50', '$100'],
118
- ['Gadget', '1', '$75', '$75']
119
- ];
120
-
121
- let yPos = height - 150;
122
- items.forEach(row => {
123
- let xPos = 50;
124
- row.forEach(cell => {
125
- page.drawText(cell, {
126
- x: xPos,
127
- y: yPos,
128
- size: 12,
129
- font: helveticaFont
130
- });
131
- xPos += 120;
132
- });
133
- yPos -= 25;
134
- });
135
-
136
- const pdfBytes = await pdfDoc.save();
137
- fs.writeFileSync('created.pdf', pdfBytes);
138
- }
79
+ writer.write("outbox/trimmed.pdf")
80
+ writer.close()
139
81
  ```
140
82
 
141
- #### Advanced Merge and Split Operations
142
- ```javascript
143
- import { PDFDocument } from 'pdf-lib';
144
- import fs from 'fs';
83
+ ### Overlay / Stamp (Multi-Layer Merge)
145
84
 
146
- async function mergePDFs() {
147
- // Create new document
148
- const mergedPdf = await PDFDocument.create();
85
+ ```python
86
+ from pypdf import PdfReader, PdfWriter
149
87
 
150
- // Load source PDFs
151
- const pdf1Bytes = fs.readFileSync('doc1.pdf');
152
- const pdf2Bytes = fs.readFileSync('doc2.pdf');
88
+ base = PdfReader("base.pdf")
89
+ stamp = PdfReader("stamp.pdf")
90
+ writer = PdfWriter()
153
91
 
154
- const pdf1 = await PDFDocument.load(pdf1Bytes);
155
- const pdf2 = await PDFDocument.load(pdf2Bytes);
92
+ for page in base.pages:
93
+ page.merge_page(stamp.pages[0]) # Stamp page 1 onto every page
94
+ writer.add_page(page)
156
95
 
157
- // Copy pages from first PDF
158
- const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
159
- pdf1Pages.forEach(page => mergedPdf.addPage(page));
96
+ writer.write("outbox/stamped.pdf")
97
+ writer.close()
98
+ ```
160
99
 
161
- // Copy specific pages from second PDF (pages 0, 2, 4)
162
- const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
163
- pdf2Pages.forEach(page => mergedPdf.addPage(page));
100
+ ---
164
101
 
165
- const mergedPdfBytes = await mergedPdf.save();
166
- fs.writeFileSync('merged.pdf', mergedPdfBytes);
167
- }
168
- ```
102
+ ## pdfplumber -- Advanced Usage
169
103
 
170
- ### pdfjs-dist (Apache License)
104
+ ### Extracting text from specific regions
171
105
 
172
- PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
106
+ ```python
107
+ import pdfplumber
173
108
 
174
- #### Basic PDF Loading and Rendering
175
- ```javascript
176
- import * as pdfjsLib from 'pdfjs-dist';
109
+ with pdfplumber.open("input.pdf") as pdf:
110
+ page = pdf.pages[0]
177
111
 
178
- // Configure worker (important for performance)
179
- pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js';
112
+ # Crop to a region (x0, top, x1, bottom) -- note: top-left origin
113
+ region = page.within_bbox((50, 100, 400, 300))
114
+ text = region.extract_text()
115
+ print(text)
116
+ ```
180
117
 
181
- async function renderPDF() {
182
- // Load PDF
183
- const loadingTask = pdfjsLib.getDocument('document.pdf');
184
- const pdf = await loadingTask.promise;
118
+ ### Table extraction with custom settings
185
119
 
186
- console.log(`Loaded PDF with ${pdf.numPages} pages`);
120
+ ```python
121
+ import pdfplumber
187
122
 
188
- // Get first page
189
- const page = await pdf.getPage(1);
190
- const viewport = page.getViewport({ scale: 1.5 });
123
+ with pdfplumber.open("input.pdf") as pdf:
124
+ page = pdf.pages[0]
191
125
 
192
- // Render to canvas
193
- const canvas = document.createElement('canvas');
194
- const context = canvas.getContext('2d');
195
- canvas.height = viewport.height;
196
- canvas.width = viewport.width;
126
+ table_settings = {
127
+ "vertical_strategy": "text",
128
+ "horizontal_strategy": "lines",
129
+ "min_words_vertical": 3,
130
+ "min_words_horizontal": 1,
131
+ }
197
132
 
198
- const renderContext = {
199
- canvasContext: context,
200
- viewport: viewport
201
- };
133
+ tables = page.extract_tables(table_settings)
134
+ for table in tables:
135
+ for row in table:
136
+ print(row)
137
+ ```
202
138
 
203
- await page.render(renderContext).promise;
204
- document.body.appendChild(canvas);
205
- }
139
+ ### Getting character-level data
140
+
141
+ ```python
142
+ import pdfplumber
143
+
144
+ with pdfplumber.open("input.pdf") as pdf:
145
+ page = pdf.pages[0]
146
+
147
+ for char in page.chars:
148
+ print(f"'{char['text']}' at ({char['x0']:.1f}, {char['top']:.1f}) "
149
+ f"font={char['fontname']} size={char['size']:.1f}")
206
150
  ```
207
151
 
208
- #### Extract Text with Coordinates
209
- ```javascript
210
- import * as pdfjsLib from 'pdfjs-dist';
152
+ ---
211
153
 
212
- async function extractText() {
213
- const loadingTask = pdfjsLib.getDocument('document.pdf');
214
- const pdf = await loadingTask.promise;
154
+ ## reportlab -- Advanced Usage
215
155
 
216
- let fullText = '';
156
+ ### Drawing tables
217
157
 
218
- // Extract text from all pages
219
- for (let i = 1; i <= pdf.numPages; i++) {
220
- const page = await pdf.getPage(i);
221
- const textContent = await page.getTextContent();
158
+ ```python
159
+ from reportlab.lib.pagesizes import letter
160
+ from reportlab.lib.units import inch
161
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
162
+ from reportlab.lib import colors
222
163
 
223
- const pageText = textContent.items
224
- .map(item => item.str)
225
- .join(' ');
164
+ doc = SimpleDocTemplate("outbox/table.pdf", pagesize=letter)
226
165
 
227
- fullText += `\n--- Page ${i} ---\n${pageText}`;
166
+ data = [
167
+ ["Name", "Age", "City"],
168
+ ["Alice", "30", "Toronto"],
169
+ ["Bob", "25", "Vancouver"],
170
+ ["Carol", "35", "Montreal"],
171
+ ]
228
172
 
229
- // Get text with coordinates for advanced processing
230
- const textWithCoords = textContent.items.map(item => ({
231
- text: item.str,
232
- x: item.transform[4],
233
- y: item.transform[5],
234
- width: item.width,
235
- height: item.height
236
- }));
237
- }
173
+ table = Table(data, colWidths=[2 * inch, 1 * inch, 2 * inch])
174
+ table.setStyle(TableStyle([
175
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#4472C4")),
176
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
177
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
178
+ ("FONTSIZE", (0, 0), (-1, -1), 11),
179
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
180
+ ("ALIGN", (1, 1), (1, -1), "CENTER"),
181
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#D9E2F3")]),
182
+ ]))
238
183
 
239
- console.log(fullText);
240
- return fullText;
241
- }
184
+ doc.build([table])
242
185
  ```
243
186
 
244
- #### Extract Annotations and Forms
245
- ```javascript
246
- import * as pdfjsLib from 'pdfjs-dist';
187
+ ### Adding page numbers and headers
188
+
189
+ ```python
190
+ from reportlab.lib.pagesizes import letter
191
+ from reportlab.lib.units import inch
192
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
193
+ from reportlab.lib.styles import getSampleStyleSheet
247
194
 
248
- async function extractAnnotations() {
249
- const loadingTask = pdfjsLib.getDocument('annotated.pdf');
250
- const pdf = await loadingTask.promise;
195
+ def add_page_number(canvas_obj, doc):
196
+ canvas_obj.saveState()
197
+ canvas_obj.setFont("Helvetica", 9)
198
+ page_num = canvas_obj.getPageNumber()
199
+ text = f"Page {page_num}"
200
+ canvas_obj.drawRightString(7.5 * inch, 0.5 * inch, text)
201
+ # Header
202
+ canvas_obj.drawString(1 * inch, 10.5 * inch, "My Report")
203
+ canvas_obj.line(1 * inch, 10.45 * inch, 7.5 * inch, 10.45 * inch)
204
+ canvas_obj.restoreState()
205
+
206
+ doc = SimpleDocTemplate("outbox/report.pdf", pagesize=letter)
207
+ styles = getSampleStyleSheet()
208
+ story = []
251
209
 
252
- for (let i = 1; i <= pdf.numPages; i++) {
253
- const page = await pdf.getPage(i);
254
- const annotations = await page.getAnnotations();
210
+ for i in range(5):
211
+ story.append(Paragraph(f"Content for page {i + 1}", styles["BodyText"]))
212
+ story.append(PageBreak())
255
213
 
256
- annotations.forEach(annotation => {
257
- console.log(`Annotation type: ${annotation.subtype}`);
258
- console.log(`Content: ${annotation.contents}`);
259
- console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`);
260
- });
261
- }
262
- }
214
+ doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number)
263
215
  ```
264
216
 
265
- ## Advanced Command-Line Operations
217
+ ### Drawing shapes and diagrams
266
218
 
267
- ### poppler-utils Advanced Features
219
+ ```python
220
+ from reportlab.pdfgen import canvas
221
+ from reportlab.lib.pagesizes import letter
222
+ from reportlab.lib.units import inch
223
+ from reportlab.lib.colors import HexColor
268
224
 
269
- #### Extract Text with Bounding Box Coordinates
270
- ```bash
271
- # Extract text with bounding box coordinates (essential for structured data)
272
- pdftotext -bbox-layout document.pdf output.xml
225
+ c = canvas.Canvas("outbox/shapes.pdf", pagesize=letter)
226
+ w, h = letter
227
+
228
+ # Rounded rectangle
229
+ c.setFillColor(HexColor("#E8F0FE"))
230
+ c.setStrokeColor(HexColor("#4472C4"))
231
+ c.roundRect(1 * inch, h - 3 * inch, 3 * inch, 1.5 * inch, 10, fill=True)
273
232
 
274
- # The XML output contains precise coordinates for each text element
233
+ # Circle
234
+ c.setFillColor(HexColor("#FCE4EC"))
235
+ c.circle(5.5 * inch, h - 2.25 * inch, 0.75 * inch, fill=True)
236
+
237
+ # Arrow (as a line with a polygon head)
238
+ c.setStrokeColor(HexColor("#333333"))
239
+ c.setLineWidth(2)
240
+ c.line(4.2 * inch, h - 2.25 * inch, 4.6 * inch, h - 2.25 * inch)
241
+
242
+ c.save()
275
243
  ```
276
244
 
277
- #### Advanced Image Conversion
278
- ```bash
279
- # Convert to PNG images with specific resolution
280
- pdftoppm -png -r 300 document.pdf output_prefix
245
+ ---
281
246
 
282
- # Convert specific page range with high resolution
283
- pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
247
+ ## Advanced Techniques
284
248
 
285
- # Convert to JPEG with quality setting
286
- pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output
249
+ ### Redaction (requires pymupdf)
250
+
251
+ To permanently remove content from a PDF, pymupdf (fitz) provides a redaction API. Install with `pip install pymupdf`.
252
+
253
+ ```python
254
+ import fitz
255
+
256
+ doc = fitz.open("input.pdf")
257
+ for page in doc:
258
+ for rect in page.search_for("CONFIDENTIAL"):
259
+ page.add_redact_annot(rect, fill=(0, 0, 0))
260
+ page.apply_redactions()
261
+ doc.save("outbox/redacted.pdf")
262
+ doc.close()
287
263
  ```
288
264
 
289
- #### Extract Embedded Images
290
- ```bash
291
- # Extract all embedded images with metadata
292
- pdfimages -j -p document.pdf page_images
265
+ **Warning**: `apply_redactions()` permanently removes the underlying content. This is irreversible. Always work on a copy.
266
+
267
+ ### Adding Bookmarks / Table of Contents (requires pymupdf)
268
+
269
+ ```python
270
+ import fitz
271
+
272
+ doc = fitz.open("input.pdf")
273
+
274
+ # Read existing TOC
275
+ toc = doc.get_toc() # list of [level, title, page_number]
276
+
277
+ # Set new TOC
278
+ new_toc = [
279
+ [1, "Chapter 1", 1],
280
+ [2, "Section 1.1", 1],
281
+ [2, "Section 1.2", 3],
282
+ [1, "Chapter 2", 5],
283
+ ]
284
+ doc.set_toc(new_toc)
293
285
 
294
- # List image info without extracting
295
- pdfimages -list document.pdf
286
+ doc.save("outbox/with_bookmarks.pdf")
287
+ doc.close()
288
+ ```
289
+
290
+ ### Text Search and Highlight (requires pymupdf)
296
291
 
297
- # Extract images in their original format
298
- pdfimages -all document.pdf images/img
292
+ ```python
293
+ import fitz
294
+
295
+ doc = fitz.open("input.pdf")
296
+ for page in doc:
297
+ matches = page.search_for("search term")
298
+ for rect in matches:
299
+ page.add_highlight_annot(rect)
300
+ doc.save("outbox/highlighted.pdf")
301
+ doc.close()
299
302
  ```
300
303
 
301
- ### qpdf Advanced Features
304
+ ### Creating Searchable PDFs from Scans
305
+
306
+ For multi-page scanned PDFs, use `ocrmypdf` (install with `pip install ocrmypdf`):
302
307
 
303
- #### Complex Page Manipulation
304
308
  ```bash
305
- # Split PDF into groups of pages
306
- qpdf --split-pages=3 input.pdf output_group_%02d.pdf
309
+ ocrmypdf scanned.pdf searchable.pdf
310
+ ```
307
311
 
308
- # Extract specific pages with complex ranges
309
- qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
312
+ For single pages, use pytesseract directly:
310
313
 
311
- # Merge specific pages from multiple PDFs
312
- qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf
314
+ ```python
315
+ from pdf2image import convert_from_path
316
+ import pytesseract
317
+
318
+ images = convert_from_path("scanned.pdf", dpi=300)
319
+ pdf_bytes = pytesseract.image_to_pdf_or_hocr(images[0], extension="pdf")
320
+ with open("outbox/searchable_page.pdf", "wb") as f:
321
+ f.write(pdf_bytes)
313
322
  ```
314
323
 
315
- #### PDF Optimization and Repair
324
+ ---
325
+
326
+ ## CLI Tools Reference
327
+
328
+ ### qpdf
329
+
316
330
  ```bash
317
- # Optimize PDF for web (linearize for streaming)
318
- qpdf --linearize input.pdf optimized.pdf
331
+ # Merge
332
+ qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
333
+
334
+ # Split every page
335
+ qpdf input.pdf --split-pages output_%d.pdf
336
+
337
+ # Extract pages 3-7
338
+ qpdf input.pdf --pages . 3-7 -- extracted.pdf
339
+
340
+ # Extract complex ranges
341
+ qpdf input.pdf --pages . 1,3-5,8,10-end -- extracted.pdf
319
342
 
320
- # Remove unused objects and compress
321
- qpdf --optimize-level=all input.pdf compressed.pdf
343
+ # Rotate page 1 by 90 degrees
344
+ qpdf input.pdf --rotate=+90:1 rotated.pdf
322
345
 
323
- # Attempt to repair corrupted PDF structure
346
+ # Rotate all pages
347
+ qpdf input.pdf --rotate=+90 -- rotated.pdf
348
+
349
+ # Linearize for fast web display
350
+ qpdf --linearize input.pdf optimized.pdf
351
+
352
+ # Check/repair
324
353
  qpdf --check input.pdf
325
- qpdf --fix-qdf damaged.pdf repaired.pdf
354
+ qpdf input.pdf --replace-input # repair in place
326
355
 
327
- # Show detailed PDF structure for debugging
328
- qpdf --show-all-pages input.pdf > structure.txt
329
- ```
356
+ # Encrypt AES-256
357
+ qpdf --encrypt userpass ownerpass 256 -- input.pdf encrypted.pdf
330
358
 
331
- #### Advanced Encryption
332
- ```bash
333
- # Add password protection with specific permissions
359
+ # Encrypt with restricted permissions
334
360
  qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
335
361
 
362
+ # Decrypt
363
+ qpdf --decrypt --password=pass encrypted.pdf decrypted.pdf
364
+
336
365
  # Check encryption status
337
366
  qpdf --show-encryption encrypted.pdf
338
367
 
339
- # Remove password protection (requires password)
340
- qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf
368
+ # Flatten annotations (bake form values)
369
+ qpdf --flatten-annotations=all input.pdf flattened.pdf
370
+
371
+ # Remove restrictions (if no encryption)
372
+ qpdf --decrypt input.pdf unrestricted.pdf
341
373
  ```
342
374
 
343
- ## Advanced Python Techniques
375
+ ### pdftotext (poppler)
344
376
 
345
- ### pdfplumber Advanced Features
377
+ ```bash
378
+ # Basic extraction
379
+ pdftotext input.pdf output.txt
346
380
 
347
- #### Extract Text with Precise Coordinates
348
- ```python
349
- import pdfplumber
381
+ # Preserve layout
382
+ pdftotext -layout input.pdf output.txt
350
383
 
351
- with pdfplumber.open("document.pdf") as pdf:
352
- page = pdf.pages[0]
353
-
354
- # Extract all text with coordinates
355
- chars = page.chars
356
- for char in chars[:10]: # First 10 characters
357
- print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}")
358
-
359
- # Extract text by bounding box (left, top, right, bottom)
360
- bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()
361
- ```
384
+ # Page range
385
+ pdftotext -f 2 -l 10 input.pdf output.txt
362
386
 
363
- #### Advanced Table Extraction with Custom Settings
364
- ```python
365
- import pdfplumber
366
- import pandas as pd
387
+ # Output to stdout
388
+ pdftotext input.pdf -
367
389
 
368
- with pdfplumber.open("complex_table.pdf") as pdf:
369
- page = pdf.pages[0]
370
-
371
- # Extract tables with custom settings for complex layouts
372
- table_settings = {
373
- "vertical_strategy": "lines",
374
- "horizontal_strategy": "lines",
375
- "snap_tolerance": 3,
376
- "intersection_tolerance": 15
377
- }
378
- tables = page.extract_tables(table_settings)
379
-
380
- # Visual debugging for table extraction
381
- img = page.to_image(resolution=150)
382
- img.save("debug_layout.png")
383
- ```
390
+ # HTML output
391
+ pdftotext -htmlmeta input.pdf output.html
384
392
 
385
- ### reportlab Advanced Features
393
+ # Encoding
394
+ pdftotext -enc UTF-8 input.pdf output.txt
386
395
 
387
- #### Create Professional Reports with Tables
388
- ```python
389
- from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
390
- from reportlab.lib.styles import getSampleStyleSheet
391
- from reportlab.lib import colors
396
+ # Extract with bounding box coordinates (XML)
397
+ pdftotext -bbox-layout input.pdf output.xml
398
+ ```
392
399
 
393
- # Sample data
394
- data = [
395
- ['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
396
- ['Widgets', '120', '135', '142', '158'],
397
- ['Gadgets', '85', '92', '98', '105']
398
- ]
400
+ ### pdfimages (poppler)
399
401
 
400
- # Create PDF with table
401
- doc = SimpleDocTemplate("report.pdf")
402
- elements = []
402
+ ```bash
403
+ # Extract as PNG
404
+ pdfimages -png input.pdf outdir/prefix
403
405
 
404
- # Add title
405
- styles = getSampleStyleSheet()
406
- title = Paragraph("Quarterly Sales Report", styles['Title'])
407
- elements.append(title)
406
+ # Extract in original format
407
+ pdfimages -all input.pdf outdir/prefix
408
408
 
409
- # Add table with advanced styling
410
- table = Table(data)
411
- table.setStyle(TableStyle([
412
- ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
413
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
414
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
415
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
416
- ('FONTSIZE', (0, 0), (-1, 0), 14),
417
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
418
- ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
419
- ('GRID', (0, 0), (-1, -1), 1, colors.black)
420
- ]))
421
- elements.append(table)
409
+ # List images (no extraction)
410
+ pdfimages -list input.pdf
422
411
 
423
- doc.build(elements)
412
+ # Specific page range
413
+ pdfimages -f 1 -l 5 -png input.pdf outdir/prefix
424
414
  ```
425
415
 
426
- ## Complex Workflows
416
+ ### tesseract (OCR)
427
417
 
428
- ### Extract Figures/Images from PDF
418
+ ```bash
419
+ # Basic OCR
420
+ tesseract image.png output_base # produces output_base.txt
421
+
422
+ # Specify language
423
+ tesseract image.png output_base -l eng+fra
424
+
425
+ # Output formats
426
+ tesseract image.png output_base pdf # searchable PDF
427
+ tesseract image.png output_base hocr # HTML with coordinates
428
+ tesseract image.png output_base tsv # tab-separated values
429
+
430
+ # Page segmentation modes (--psm)
431
+ # 3 = fully automatic (default)
432
+ # 6 = assume uniform block of text
433
+ # 7 = single text line
434
+ # 8 = single word
435
+ tesseract image.png output_base --psm 6
436
+
437
+ # List available languages
438
+ tesseract --list-langs
439
+ ```
440
+
441
+ ### pdftoppm (poppler -- image conversion)
429
442
 
430
- #### Method 1: Using pdfimages (fastest)
431
443
  ```bash
432
- # Extract all images with original quality
433
- pdfimages -all document.pdf images/img
444
+ # Convert to PNG at 300 DPI
445
+ pdftoppm -png -r 300 input.pdf output_prefix
446
+
447
+ # Convert specific pages
448
+ pdftoppm -png -r 300 -f 1 -l 3 input.pdf output_prefix
449
+
450
+ # Convert to JPEG with quality setting
451
+ pdftoppm -jpeg -jpegopt quality=85 -r 200 input.pdf output_prefix
434
452
  ```
435
453
 
436
- #### Method 2: Using pypdfium2 + Image Processing
454
+ ---
455
+
456
+ ## Batch Processing
457
+
458
+ ### Process many PDFs in parallel
459
+
437
460
  ```python
438
- import pypdfium2 as pdfium
439
- from PIL import Image
440
- import numpy as np
441
-
442
- def extract_figures(pdf_path, output_dir):
443
- pdf = pdfium.PdfDocument(pdf_path)
444
-
445
- for page_num, page in enumerate(pdf):
446
- # Render high-resolution page
447
- bitmap = page.render(scale=3.0)
448
- img = bitmap.to_pil()
449
-
450
- # Convert to numpy for processing
451
- img_array = np.array(img)
452
-
453
- # Simple figure detection (non-white regions)
454
- mask = np.any(img_array != [255, 255, 255], axis=2)
455
-
456
- # Find contours and extract bounding boxes
457
- # (This is simplified - real implementation would need more sophisticated detection)
458
-
459
- # Save detected figures
460
- # ... implementation depends on specific needs
461
+ from pathlib import Path
462
+ from concurrent.futures import ProcessPoolExecutor
463
+ from pypdf import PdfReader
464
+
465
+ def extract_text(pdf_path: str) -> tuple[str, str]:
466
+ reader = PdfReader(pdf_path)
467
+ text = "\n".join(page.extract_text() or "" for page in reader.pages)
468
+ return pdf_path, text
469
+
470
+ pdf_files = list(Path("inbox").glob("*.pdf"))
471
+
472
+ with ProcessPoolExecutor(max_workers=4) as executor:
473
+ results = executor.map(extract_text, [str(p) for p in pdf_files])
474
+ for path, text in results:
475
+ print(f"{path}: {len(text)} chars")
461
476
  ```
462
477
 
463
- ### Batch PDF Processing with Error Handling
478
+ ### Batch processing with error handling
479
+
464
480
  ```python
465
- import os
466
- import glob
481
+ from pathlib import Path
467
482
  from pypdf import PdfReader, PdfWriter
468
- import logging
469
-
470
- logging.basicConfig(level=logging.INFO)
471
- logger = logging.getLogger(__name__)
472
-
473
- def batch_process_pdfs(input_dir, operation='merge'):
474
- pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
475
-
476
- if operation == 'merge':
477
- writer = PdfWriter()
478
- for pdf_file in pdf_files:
479
- try:
480
- reader = PdfReader(pdf_file)
481
- for page in reader.pages:
482
- writer.add_page(page)
483
- logger.info(f"Processed: {pdf_file}")
484
- except Exception as e:
485
- logger.error(f"Failed to process {pdf_file}: {e}")
486
- continue
487
-
488
- with open("batch_merged.pdf", "wb") as output:
489
- writer.write(output)
490
-
491
- elif operation == 'extract_text':
492
- for pdf_file in pdf_files:
493
- try:
494
- reader = PdfReader(pdf_file)
495
- text = ""
496
- for page in reader.pages:
497
- text += page.extract_text()
498
-
499
- output_file = pdf_file.replace('.pdf', '.txt')
500
- with open(output_file, 'w', encoding='utf-8') as f:
501
- f.write(text)
502
- logger.info(f"Extracted text from: {pdf_file}")
503
-
504
- except Exception as e:
505
- logger.error(f"Failed to extract text from {pdf_file}: {e}")
506
- continue
483
+
484
+ def process_pdfs(input_dir, output_dir, operation):
485
+ """Apply an operation to all PDFs in a directory."""
486
+ Path(output_dir).mkdir(parents=True, exist_ok=True)
487
+ for pdf_path in sorted(Path(input_dir).glob("*.pdf")):
488
+ try:
489
+ reader = PdfReader(str(pdf_path))
490
+ operation(reader, pdf_path, output_dir)
491
+ print(f"OK: {pdf_path.name}")
492
+ except Exception as e:
493
+ print(f"FAIL: {pdf_path.name}: {e}")
507
494
  ```
508
495
 
509
- ### Advanced PDF Cropping
510
- ```python
511
- from pypdf import PdfWriter, PdfReader
496
+ ---
512
497
 
513
- reader = PdfReader("input.pdf")
514
- writer = PdfWriter()
498
+ ## Performance Tips
515
499
 
516
- # Crop page (left, bottom, right, top in points)
517
- page = reader.pages[0]
518
- page.mediabox.left = 50
519
- page.mediabox.bottom = 50
520
- page.mediabox.right = 550
521
- page.mediabox.top = 750
500
+ ### Choose the right rendering library
522
501
 
523
- writer.add_page(page)
524
- with open("cropped.pdf", "wb") as output:
525
- writer.write(output)
526
- ```
502
+ | Task | Use | Why |
503
+ |---|---|---|
504
+ | Render to image | pypdfium2 | 5-10x faster than pdf2image, no poppler dependency |
505
+ | Render many pages | pypdfium2 | Lower memory, C-based |
506
+ | Need exact poppler compat | pdf2image | Wraps poppler's pdftoppm |
527
507
 
528
- ## Performance Optimization Tips
508
+ ### Memory efficiency for large PDFs
529
509
 
530
- ### 1. For Large PDFs
531
- - Use streaming approaches instead of loading entire PDF in memory
532
- - Use `qpdf --split-pages` for splitting large files
533
- - Process pages individually with pypdfium2
510
+ ```python
511
+ from pypdf import PdfReader, PdfWriter
534
512
 
535
- ### 2. For Text Extraction
536
- - `pdftotext -bbox-layout` is fastest for plain text extraction
537
- - Use pdfplumber for structured data and tables
538
- - Avoid `pypdf.extract_text()` for very large documents
513
+ reader = PdfReader("large.pdf")
539
514
 
540
- ### 3. For Image Extraction
541
- - `pdfimages` is much faster than rendering pages
542
- - Use low resolution for previews, high resolution for final output
515
+ # Process pages one at a time instead of loading all
516
+ for i in range(len(reader.pages)):
517
+ writer = PdfWriter()
518
+ writer.add_page(reader.pages[i])
543
519
 
544
- ### 4. For Form Filling
545
- - pdf-lib maintains form structure better than most alternatives
546
- - Pre-validate form fields before processing
520
+ text = reader.pages[i].extract_text()
521
+ # process text...
547
522
 
548
- ### 5. Memory Management
549
- ```python
550
- # Process PDFs in chunks
551
- def process_large_pdf(pdf_path, chunk_size=10):
552
- reader = PdfReader(pdf_path)
553
- total_pages = len(reader.pages)
554
-
555
- for start_idx in range(0, total_pages, chunk_size):
556
- end_idx = min(start_idx + chunk_size, total_pages)
557
- writer = PdfWriter()
558
-
559
- for i in range(start_idx, end_idx):
560
- writer.add_page(reader.pages[i])
561
-
562
- # Process chunk
563
- with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
564
- writer.write(output)
523
+ writer.close() # free memory
565
524
  ```
566
525
 
567
- ## Troubleshooting Common Issues
526
+ ### Compress PDF with Ghostscript
568
527
 
569
- ### Encrypted PDFs
570
- ```python
571
- # Handle password-protected PDFs
572
- from pypdf import PdfReader
573
-
574
- try:
575
- reader = PdfReader("encrypted.pdf")
576
- if reader.is_encrypted:
577
- reader.decrypt("password")
578
- except Exception as e:
579
- print(f"Failed to decrypt: {e}")
528
+ ```bash
529
+ gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook \
530
+ -dNOPAUSE -dBATCH -sOutputFile=compressed.pdf input.pdf
580
531
  ```
581
532
 
582
- ### Corrupted PDFs
533
+ Settings: `/screen` (72dpi), `/ebook` (150dpi), `/printer` (300dpi), `/prepress` (300dpi, color-preserving).
534
+
535
+ ### Repair damaged PDFs
536
+
583
537
  ```bash
584
- # Use qpdf to repair
585
- qpdf --check corrupted.pdf
586
- qpdf --replace-input corrupted.pdf
538
+ qpdf --replace-input damaged.pdf # In-place repair
539
+ qpdf damaged.pdf repaired.pdf # qpdf fixes many structural issues automatically
540
+ gs -o repaired.pdf -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress damaged.pdf # Ghostscript rewrite
587
541
  ```
588
542
 
589
- ### Text Extraction Issues
590
- ```python
591
- # Fallback to OCR for scanned PDFs
592
- import pytesseract
593
- from pdf2image import convert_from_path
543
+ ---
544
+
545
+ ## Coordinate Systems
594
546
 
595
- def extract_text_with_ocr(pdf_path):
596
- images = convert_from_path(pdf_path)
597
- text = ""
598
- for i, image in enumerate(images):
599
- text += pytesseract.image_to_string(image)
600
- return text
547
+ PDF coordinates can be confusing. Key facts:
548
+
549
+ - **PDF native**: origin at bottom-left, Y increases upward. Units are points (1 pt = 1/72 inch).
550
+ - **pdfplumber**: origin at top-left, Y increases downward (like screen coordinates). Uses the same point scale.
551
+ - **Rendered images**: origin at top-left, Y increases downward. Units are pixels. Conversion: `pdf_points = pixels * 72 / dpi`.
552
+
553
+ ### Converting between systems
554
+
555
+ ```python
556
+ def image_coords_to_pdf(pixel_x, pixel_y, page_height_pts, dpi):
557
+ """Convert rendered image pixel coordinates to PDF coordinates."""
558
+ pdf_x = pixel_x * 72.0 / dpi
559
+ pdf_y = page_height_pts - (pixel_y * 72.0 / dpi)
560
+ return pdf_x, pdf_y
561
+
562
+ def pdf_coords_to_image(pdf_x, pdf_y, page_height_pts, dpi):
563
+ """Convert PDF coordinates to rendered image pixel coordinates."""
564
+ pixel_x = pdf_x * dpi / 72.0
565
+ pixel_y = (page_height_pts - pdf_y) * dpi / 72.0
566
+ return pixel_x, pixel_y
601
567
  ```
602
568
 
603
- ## License Information
569
+ ---
570
+
571
+ ## Library License Information
604
572
 
605
- - **pypdf**: BSD License
606
- - **pdfplumber**: MIT License
607
- - **pypdfium2**: Apache/BSD License
608
- - **reportlab**: BSD License
609
- - **poppler-utils**: GPL-2 License
610
- - **qpdf**: Apache License
611
- - **pdf-lib**: MIT License
612
- - **pdfjs-dist**: Apache License
573
+ | Library | License |
574
+ |---|---|
575
+ | pypdf | BSD |
576
+ | pdfplumber | MIT |
577
+ | pypdfium2 | Apache/BSD |
578
+ | reportlab | BSD |
579
+ | pytesseract | Apache-2.0 |
580
+ | Pillow | MIT-like (HPND) |
581
+ | poppler-utils | GPL-2 |
582
+ | qpdf | Apache |