hive-skills 0.1.0
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.
- package/LICENSE +28 -0
- package/README.md +235 -0
- package/assets/LICENSE +32 -0
- package/assets/THIRD_PARTY_NOTICES.md +72 -0
- package/assets/manifest.json +1290 -0
- package/assets/skills/authored/code-review/composable/BUNDLE.md +192 -0
- package/assets/skills/authored/code-review/composable/INDEX.md +13 -0
- package/assets/skills/authored/code-review/composable/VERSION +1 -0
- package/assets/skills/authored/code-review/composable/mini/01-review-method.md +34 -0
- package/assets/skills/authored/code-review/composable/mini/02-security-review.md +20 -0
- package/assets/skills/authored/code-review/composable/mini/03-correctness-bugs.md +19 -0
- package/assets/skills/authored/code-review/composable/mini/04-concurrency.md +16 -0
- package/assets/skills/authored/code-review/composable/mini/05-error-handling.md +12 -0
- package/assets/skills/authored/code-review/composable/mini/06-performance.md +11 -0
- package/assets/skills/authored/code-review/composable/mini/07-api-design.md +20 -0
- package/assets/skills/authored/code-review/composable/mini/08-missing-tests.md +25 -0
- package/assets/skills/authored/code-review/composable/mini/09-refactoring-and-communication.md +32 -0
- package/assets/skills/authored/data-analysis/composable/BUNDLE.md +186 -0
- package/assets/skills/authored/data-analysis/composable/INDEX.md +13 -0
- package/assets/skills/authored/data-analysis/composable/VERSION +1 -0
- package/assets/skills/authored/data-analysis/composable/mini/01-data-quality-profiling.md +26 -0
- package/assets/skills/authored/data-analysis/composable/mini/02-aggregation-pitfalls.md +21 -0
- package/assets/skills/authored/data-analysis/composable/mini/03-time-series.md +19 -0
- package/assets/skills/authored/data-analysis/composable/mini/04-segmentation.md +23 -0
- package/assets/skills/authored/data-analysis/composable/mini/05-discount-pricing.md +17 -0
- package/assets/skills/authored/data-analysis/composable/mini/06-insight-generation.md +25 -0
- package/assets/skills/authored/data-analysis/composable/mini/07-executive-writing.md +26 -0
- package/assets/skills/authored/data-analysis/composable/mini/08-recommendations.md +20 -0
- package/assets/skills/authored/data-analysis/composable/mini/09-reproducibility-assumptions.md +20 -0
- package/assets/skills/authored/financial-analysis/composable/BUNDLE.md +1293 -0
- package/assets/skills/authored/financial-analysis/composable/INDEX.md +17 -0
- package/assets/skills/authored/financial-analysis/composable/VERSION +1 -0
- package/assets/skills/authored/financial-analysis/composable/mini/00-core.md +69 -0
- package/assets/skills/authored/financial-analysis/composable/mini/01-profitability-efficiency-ratios.md +173 -0
- package/assets/skills/authored/financial-analysis/composable/mini/02-liquidity-leverage-ratios.md +120 -0
- package/assets/skills/authored/financial-analysis/composable/mini/03-valuation-ratios-interpretation.md +107 -0
- package/assets/skills/authored/financial-analysis/composable/mini/04-dcf-projection-wacc.md +122 -0
- package/assets/skills/authored/financial-analysis/composable/mini/05-dcf-terminal-value-sensitivity.md +75 -0
- package/assets/skills/authored/financial-analysis/composable/mini/06-comparables-precedents.md +78 -0
- package/assets/skills/authored/financial-analysis/composable/mini/07-budget-variance-analysis.md +47 -0
- package/assets/skills/authored/financial-analysis/composable/mini/08-driver-based-forecasting-scenarios.md +111 -0
- package/assets/skills/authored/financial-analysis/composable/mini/09-rolling-forecasts-accuracy.md +114 -0
- package/assets/skills/authored/financial-analysis/composable/mini/10-reporting-exec-summary.md +58 -0
- package/assets/skills/authored/financial-analysis/composable/mini/11-industry-adaptations.md +130 -0
- package/assets/skills/authored/financial-analysis/composable/mini/12-toolkit-scripts-io.md +102 -0
- package/assets/skills/authored/python-api/composable/BUNDLE.md +389 -0
- package/assets/skills/authored/python-api/composable/INDEX.md +13 -0
- package/assets/skills/authored/python-api/composable/VERSION +1 -0
- package/assets/skills/authored/python-api/composable/mini/01-project-structure.md +44 -0
- package/assets/skills/authored/python-api/composable/mini/02-routing-and-app.md +38 -0
- package/assets/skills/authored/python-api/composable/mini/03-pydantic-models.md +45 -0
- package/assets/skills/authored/python-api/composable/mini/04-validation-and-errors.md +59 -0
- package/assets/skills/authored/python-api/composable/mini/05-dependencies-and-auth.md +60 -0
- package/assets/skills/authored/python-api/composable/mini/06-pagination-filtering.md +44 -0
- package/assets/skills/authored/python-api/composable/mini/07-async-performance.md +30 -0
- package/assets/skills/authored/python-api/composable/mini/08-testing.md +45 -0
- package/assets/skills/authored/python-api/composable/mini/09-middleware-observability.md +29 -0
- package/assets/skills/authored/tech-writing/composable/BUNDLE.md +220 -0
- package/assets/skills/authored/tech-writing/composable/INDEX.md +12 -0
- package/assets/skills/authored/tech-writing/composable/VERSION +1 -0
- package/assets/skills/authored/tech-writing/composable/mini/01-audience-analysis.md +26 -0
- package/assets/skills/authored/tech-writing/composable/mini/02-document-types.md +27 -0
- package/assets/skills/authored/tech-writing/composable/mini/03-readme-quickstart-changelog.md +22 -0
- package/assets/skills/authored/tech-writing/composable/mini/04-information-architecture.md +21 -0
- package/assets/skills/authored/tech-writing/composable/mini/05-style-clarity.md +29 -0
- package/assets/skills/authored/tech-writing/composable/mini/06-breaking-changes-migrations.md +33 -0
- package/assets/skills/authored/tech-writing/composable/mini/07-troubleshooting-security.md +30 -0
- package/assets/skills/authored/tech-writing/composable/mini/08-faq-voice-tone.md +32 -0
- package/assets/skills/converted/claude-api/composable/BUNDLE.md +14996 -0
- package/assets/skills/converted/claude-api/composable/INDEX.md +98 -0
- package/assets/skills/converted/claude-api/composable/VERSION +1 -0
- package/assets/skills/converted/claude-api/composable/mini/00-core.md +235 -0
- package/assets/skills/converted/claude-api/composable/mini/01-model-api-quick-reference.md +258 -0
- package/assets/skills/converted/claude-api/composable/mini/02-common-pitfalls.md +42 -0
- package/assets/skills/converted/claude-api/composable/mini/10-models.md +135 -0
- package/assets/skills/converted/claude-api/composable/mini/11-platform-availability.md +96 -0
- package/assets/skills/converted/claude-api/composable/mini/12-claude-platform-on-aws.md +59 -0
- package/assets/skills/converted/claude-api/composable/mini/13-live-sources.md +143 -0
- package/assets/skills/converted/claude-api/composable/mini/14-error-codes.md +256 -0
- package/assets/skills/converted/claude-api/composable/mini/15-prompt-caching.md +223 -0
- package/assets/skills/converted/claude-api/composable/mini/16-token-counting.md +56 -0
- package/assets/skills/converted/claude-api/composable/mini/17-tool-use-concepts.md +444 -0
- package/assets/skills/converted/claude-api/composable/mini/18-agent-design.md +101 -0
- package/assets/skills/converted/claude-api/composable/mini/19-anthropic-cli.md +246 -0
- package/assets/skills/converted/claude-api/composable/mini/20-model-migration.md +1301 -0
- package/assets/skills/converted/claude-api/composable/mini/30-managed-agents-overview.md +71 -0
- package/assets/skills/converted/claude-api/composable/mini/31-managed-agents-core.md +278 -0
- package/assets/skills/converted/claude-api/composable/mini/32-managed-agents-environments.md +219 -0
- package/assets/skills/converted/claude-api/composable/mini/33-managed-agents-tools.md +366 -0
- package/assets/skills/converted/claude-api/composable/mini/34-managed-agents-events.md +252 -0
- package/assets/skills/converted/claude-api/composable/mini/35-managed-agents-outcomes.md +106 -0
- package/assets/skills/converted/claude-api/composable/mini/36-managed-agents-multiagent.md +101 -0
- package/assets/skills/converted/claude-api/composable/mini/37-managed-agents-webhooks.md +123 -0
- package/assets/skills/converted/claude-api/composable/mini/38-managed-agents-memory.md +197 -0
- package/assets/skills/converted/claude-api/composable/mini/39-managed-agents-scheduled-deployments.md +146 -0
- package/assets/skills/converted/claude-api/composable/mini/40-managed-agents-client-patterns.md +211 -0
- package/assets/skills/converted/claude-api/composable/mini/41-managed-agents-onboarding.md +82 -0
- package/assets/skills/converted/claude-api/composable/mini/42-managed-agents-api-reference.md +441 -0
- package/assets/skills/converted/claude-api/composable/mini/43-managed-agents-self-hosted-sandboxes.md +174 -0
- package/assets/skills/converted/claude-api/composable/mini/50-python-readme.md +564 -0
- package/assets/skills/converted/claude-api/composable/mini/51-python-tool-use.md +590 -0
- package/assets/skills/converted/claude-api/composable/mini/52-python-streaming-batches-files.md +549 -0
- package/assets/skills/converted/claude-api/composable/mini/53-python-managed-agents.md +335 -0
- package/assets/skills/converted/claude-api/composable/mini/55-typescript-readme.md +404 -0
- package/assets/skills/converted/claude-api/composable/mini/56-typescript-tool-use.md +548 -0
- package/assets/skills/converted/claude-api/composable/mini/57-typescript-streaming-batches-files.md +384 -0
- package/assets/skills/converted/claude-api/composable/mini/58-typescript-managed-agents.md +358 -0
- package/assets/skills/converted/claude-api/composable/mini/60-csharp-readme.md +361 -0
- package/assets/skills/converted/claude-api/composable/mini/61-csharp-tool-use.md +163 -0
- package/assets/skills/converted/claude-api/composable/mini/62-csharp-streaming-batches-files.md +64 -0
- package/assets/skills/converted/claude-api/composable/mini/65-go-readme.md +185 -0
- package/assets/skills/converted/claude-api/composable/mini/66-go-tool-use.md +219 -0
- package/assets/skills/converted/claude-api/composable/mini/67-go-streaming-files.md +63 -0
- package/assets/skills/converted/claude-api/composable/mini/68-go-managed-agents.md +564 -0
- package/assets/skills/converted/claude-api/composable/mini/70-java-readme.md +237 -0
- package/assets/skills/converted/claude-api/composable/mini/71-java-tool-use.md +227 -0
- package/assets/skills/converted/claude-api/composable/mini/72-java-streaming-files.md +49 -0
- package/assets/skills/converted/claude-api/composable/mini/73-java-managed-agents.md +443 -0
- package/assets/skills/converted/claude-api/composable/mini/75-php-readme.md +173 -0
- package/assets/skills/converted/claude-api/composable/mini/76-php-tool-use.md +252 -0
- package/assets/skills/converted/claude-api/composable/mini/77-php-streaming-batches-files.md +53 -0
- package/assets/skills/converted/claude-api/composable/mini/78-php-managed-agents.md +443 -0
- package/assets/skills/converted/claude-api/composable/mini/80-ruby-readme.md +133 -0
- package/assets/skills/converted/claude-api/composable/mini/81-ruby-tool-use-streaming.md +56 -0
- package/assets/skills/converted/claude-api/composable/mini/82-ruby-managed-agents.md +394 -0
- package/assets/skills/converted/claude-api/composable/mini/85-curl-examples.md +255 -0
- package/assets/skills/converted/claude-api/composable/mini/86-curl-managed-agents.md +340 -0
- package/assets/skills/converted/claude-api/composable/presets/csharp.md +1151 -0
- package/assets/skills/converted/claude-api/composable/presets/curl.md +1154 -0
- package/assets/skills/converted/claude-api/composable/presets/go.md +1598 -0
- package/assets/skills/converted/claude-api/composable/presets/java.md +1523 -0
- package/assets/skills/converted/claude-api/composable/presets/php.md +1488 -0
- package/assets/skills/converted/claude-api/composable/presets/python.md +2605 -0
- package/assets/skills/converted/claude-api/composable/presets/ruby.md +1146 -0
- package/assets/skills/converted/claude-api/composable/presets/shared.md +6478 -0
- package/assets/skills/converted/claude-api/composable/presets/typescript.md +2261 -0
- package/assets/skills/converted/docx/composable/BUNDLE.md +617 -0
- package/assets/skills/converted/docx/composable/INDEX.md +12 -0
- package/assets/skills/converted/docx/composable/VERSION +1 -0
- package/assets/skills/converted/docx/composable/mini/00-core.md +26 -0
- package/assets/skills/converted/docx/composable/mini/01-reading-converting.md +36 -0
- package/assets/skills/converted/docx/composable/mini/02-creating-setup-layout.md +85 -0
- package/assets/skills/converted/docx/composable/mini/03-creating-elements.md +257 -0
- package/assets/skills/converted/docx/composable/mini/04-editing-existing.md +57 -0
- package/assets/skills/converted/docx/composable/mini/05-xml-reference.md +128 -0
- package/assets/skills/converted/internal-comms/composable/BUNDLE.md +220 -0
- package/assets/skills/converted/internal-comms/composable/INDEX.md +11 -0
- package/assets/skills/converted/internal-comms/composable/VERSION +1 -0
- package/assets/skills/converted/internal-comms/composable/mini/00-core.md +30 -0
- package/assets/skills/converted/internal-comms/composable/mini/01-3p-updates.md +49 -0
- package/assets/skills/converted/internal-comms/composable/mini/02-company-newsletter.md +67 -0
- package/assets/skills/converted/internal-comms/composable/mini/03-faq-answers.md +32 -0
- package/assets/skills/converted/internal-comms/composable/mini/04-general-comms.md +18 -0
- package/assets/skills/converted/mcp-builder/composable/BUNDLE.md +2460 -0
- package/assets/skills/converted/mcp-builder/composable/INDEX.md +23 -0
- package/assets/skills/converted/mcp-builder/composable/VERSION +1 -0
- package/assets/skills/converted/mcp-builder/composable/mini/00-core.md +43 -0
- package/assets/skills/converted/mcp-builder/composable/mini/01-workflow-research-planning.md +54 -0
- package/assets/skills/converted/mcp-builder/composable/mini/02-implementation-process.md +78 -0
- package/assets/skills/converted/mcp-builder/composable/mini/03-naming-conventions.md +33 -0
- package/assets/skills/converted/mcp-builder/composable/mini/04-response-formats.md +21 -0
- package/assets/skills/converted/mcp-builder/composable/mini/05-pagination.md +24 -0
- package/assets/skills/converted/mcp-builder/composable/mini/06-transport.md +47 -0
- package/assets/skills/converted/mcp-builder/composable/mini/07-annotations.md +20 -0
- package/assets/skills/converted/mcp-builder/composable/mini/08-security.md +35 -0
- package/assets/skills/converted/mcp-builder/composable/mini/09-error-handling.md +32 -0
- package/assets/skills/converted/mcp-builder/composable/mini/10-testing-and-documentation.md +21 -0
- package/assets/skills/converted/mcp-builder/composable/mini/11-python-implementation.md +432 -0
- package/assets/skills/converted/mcp-builder/composable/mini/12-python-advanced-and-checklist.md +233 -0
- package/assets/skills/converted/mcp-builder/composable/mini/13-node-implementation.md +717 -0
- package/assets/skills/converted/mcp-builder/composable/mini/14-node-advanced-and-checklist.md +200 -0
- package/assets/skills/converted/mcp-builder/composable/mini/15-evaluation-design.md +200 -0
- package/assets/skills/converted/mcp-builder/composable/mini/16-evaluation-running.md +198 -0
- package/assets/skills/converted/mcp-builder/composable/presets/node-server.md +1381 -0
- package/assets/skills/converted/mcp-builder/composable/presets/python-server.md +1129 -0
- package/assets/skills/converted/pdf/composable/BUNDLE.md +1284 -0
- package/assets/skills/converted/pdf/composable/INDEX.md +14 -0
- package/assets/skills/converted/pdf/composable/VERSION +1 -0
- package/assets/skills/converted/pdf/composable/mini/00-core.md +46 -0
- package/assets/skills/converted/pdf/composable/mini/01-creation.md +176 -0
- package/assets/skills/converted/pdf/composable/mini/02-extraction.md +178 -0
- package/assets/skills/converted/pdf/composable/mini/03-merge-split-manipulate.md +240 -0
- package/assets/skills/converted/pdf/composable/mini/04-images-ocr.md +168 -0
- package/assets/skills/converted/pdf/composable/mini/05-encryption.md +51 -0
- package/assets/skills/converted/pdf/composable/mini/06-forms.md +300 -0
- package/assets/skills/converted/pdf/composable/mini/07-libraries-reference.md +89 -0
- package/assets/skills/converted/pptx/composable/BUNDLE.md +911 -0
- package/assets/skills/converted/pptx/composable/INDEX.md +12 -0
- package/assets/skills/converted/pptx/composable/VERSION +1 -0
- package/assets/skills/converted/pptx/composable/mini/00-core.md +37 -0
- package/assets/skills/converted/pptx/composable/mini/01-reading.md +37 -0
- package/assets/skills/converted/pptx/composable/mini/02-creating-from-scratch.md +422 -0
- package/assets/skills/converted/pptx/composable/mini/03-editing-templates.md +207 -0
- package/assets/skills/converted/pptx/composable/mini/04-design.md +89 -0
- package/assets/skills/converted/pptx/composable/mini/05-qa-and-rendering.md +91 -0
- package/assets/skills/converted/pptx/composable/presets/creating.md +659 -0
- package/assets/skills/converted/pptx/composable/presets/editing.md +485 -0
- package/assets/skills/converted/skill-creator/composable/BUNDLE.md +1681 -0
- package/assets/skills/converted/skill-creator/composable/INDEX.md +21 -0
- package/assets/skills/converted/skill-creator/composable/VERSION +1 -0
- package/assets/skills/converted/skill-creator/composable/mini/00-core.md +59 -0
- package/assets/skills/converted/skill-creator/composable/mini/01-creating-a-skill.md +27 -0
- package/assets/skills/converted/skill-creator/composable/mini/02-skill-writing-guide.md +71 -0
- package/assets/skills/converted/skill-creator/composable/mini/03-test-cases.md +21 -0
- package/assets/skills/converted/skill-creator/composable/mini/04-running-and-evaluating.md +128 -0
- package/assets/skills/converted/skill-creator/composable/mini/05-improving-the-skill.md +30 -0
- package/assets/skills/converted/skill-creator/composable/mini/06-blind-comparison.md +5 -0
- package/assets/skills/converted/skill-creator/composable/mini/07-description-optimization.md +74 -0
- package/assets/skills/converted/skill-creator/composable/mini/08-packaging.md +11 -0
- package/assets/skills/converted/skill-creator/composable/mini/09-environment-specific.md +38 -0
- package/assets/skills/converted/skill-creator/composable/mini/10-scripts-and-references.md +34 -0
- package/assets/skills/converted/skill-creator/composable/mini/11-json-schemas.md +414 -0
- package/assets/skills/converted/skill-creator/composable/mini/12-agent-grader.md +225 -0
- package/assets/skills/converted/skill-creator/composable/mini/13-agent-comparator.md +204 -0
- package/assets/skills/converted/skill-creator/composable/mini/14-agent-analyzer.md +276 -0
- package/assets/skills/converted/skill-creator/composable/presets/authoring.md +198 -0
- package/assets/skills/converted/skill-creator/composable/presets/evaluating.md +1334 -0
- package/assets/skills/converted/skill-creator/composable/presets/optimizing.md +160 -0
- package/assets/skills/converted/skill-creator/composable/presets.json +5 -0
- package/assets/skills/meta/ccs-skill-creator/composable/BUNDLE.md +465 -0
- package/assets/skills/meta/ccs-skill-creator/composable/INDEX.md +8 -0
- package/assets/skills/meta/ccs-skill-creator/composable/VERSION +1 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/00-core.md +46 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/01-create-new-skill.md +167 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/02-convert-existing-skill.md +120 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/03-review-and-maintain.md +112 -0
- package/assets/skills/sources/anthropic/PROVENANCE.md +3 -0
- package/assets/skills/sources/anthropic/claude-api/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/docx/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/internal-comms/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/mcp-builder/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/pdf/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/pptx/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/skill-creator/LICENSE.txt +202 -0
- package/assets/skills/sources/financial-analyst/PROVENANCE.md +1 -0
- package/assets/tools/hive.py +728 -0
- package/dist/cli.js +7494 -0
- package/package.json +40 -0
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
<!-- GENERATED by tools/hive.py compile. Do not hand-edit. -->
|
|
2
|
+
# pdf: Compiled Skill Bundle (v1.0.0)
|
|
3
|
+
|
|
4
|
+
All composable mini-skills for `pdf` concatenated in index order, frontmatter stripped. Regenerate with `tools/hive.py compile <skill-dir>`. Do not edit by hand.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
<!-- module: 00-core.md -->
|
|
8
|
+
|
|
9
|
+
# Core: PDF Processing
|
|
10
|
+
|
|
11
|
+
Always-loaded foundation for doing anything with PDF files using Python libraries and command-line tools. Load the focused minis (see INDEX) for creation, extraction, merging/splitting, images/OCR, encryption, form-filling, and library/performance guidance.
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
This skill covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see the extraction, merge/split, images-ocr, and libraries-reference minis. If you need to fill out a PDF form, read the forms mini and follow its instructions.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from pypdf import PdfReader, PdfWriter
|
|
21
|
+
|
|
22
|
+
# Read a PDF
|
|
23
|
+
reader = PdfReader("document.pdf")
|
|
24
|
+
print(f"Pages: {len(reader.pages)}")
|
|
25
|
+
|
|
26
|
+
# Extract text
|
|
27
|
+
text = ""
|
|
28
|
+
for page in reader.pages:
|
|
29
|
+
text += page.extract_text()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Reference
|
|
33
|
+
|
|
34
|
+
| Task | Best Tool | Command/Code |
|
|
35
|
+
|------|-----------|--------------|
|
|
36
|
+
| Merge PDFs | pypdf | `writer.add_page(page)` |
|
|
37
|
+
| Split PDFs | pypdf | One page per file |
|
|
38
|
+
| Extract text | pdfplumber | `page.extract_text()` |
|
|
39
|
+
| Extract tables | pdfplumber | `page.extract_tables()` |
|
|
40
|
+
| Create PDFs | reportlab | Canvas or Platypus |
|
|
41
|
+
| Command line merge | qpdf | `qpdf --empty --pages ...` |
|
|
42
|
+
| OCR scanned PDFs | pytesseract | Convert to image first |
|
|
43
|
+
| Fill PDF forms | pdf-lib or pypdf (see forms mini) | See forms mini |
|
|
44
|
+
|
|
45
|
+
## Vendored scripts
|
|
46
|
+
|
|
47
|
+
The form-filling helper scripts referenced by the forms mini (`check_fillable_fields.py`, `extract_form_field_info.py`, `convert_pdf_to_images.py`, `fill_fillable_fields.py`, `extract_form_structure.py`, `check_bounding_boxes.py`, `fill_pdf_form_with_annotations.py`) are vendored in the source skill at `skills/sources/anthropic/pdf/scripts/`. Command lines that say `scripts/...` refer to that vendored directory.
|
|
48
|
+
|
|
49
|
+
## Next Steps
|
|
50
|
+
|
|
51
|
+
- For advanced pypdfium2 usage, see the images-ocr and libraries-reference minis.
|
|
52
|
+
- For JavaScript libraries (pdf-lib, pdfjs-dist), see the creation, extraction, merge-split, and libraries-reference minis.
|
|
53
|
+
- If you need to fill out a PDF form, follow the instructions in the forms mini.
|
|
54
|
+
- For troubleshooting guides, see the libraries-reference mini.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
<!-- module: 01-creation.md -->
|
|
58
|
+
|
|
59
|
+
# Creating PDFs
|
|
60
|
+
|
|
61
|
+
Creating new PDF documents with reportlab (Python) and pdf-lib (JavaScript).
|
|
62
|
+
|
|
63
|
+
## reportlab - Create PDFs
|
|
64
|
+
|
|
65
|
+
### Basic PDF Creation
|
|
66
|
+
```python
|
|
67
|
+
from reportlab.lib.pagesizes import letter
|
|
68
|
+
from reportlab.pdfgen import canvas
|
|
69
|
+
|
|
70
|
+
c = canvas.Canvas("hello.pdf", pagesize=letter)
|
|
71
|
+
width, height = letter
|
|
72
|
+
|
|
73
|
+
# Add text
|
|
74
|
+
c.drawString(100, height - 100, "Hello World!")
|
|
75
|
+
c.drawString(100, height - 120, "This is a PDF created with reportlab")
|
|
76
|
+
|
|
77
|
+
# Add a line
|
|
78
|
+
c.line(100, height - 140, 400, height - 140)
|
|
79
|
+
|
|
80
|
+
# Save
|
|
81
|
+
c.save()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Create PDF with Multiple Pages
|
|
85
|
+
```python
|
|
86
|
+
from reportlab.lib.pagesizes import letter
|
|
87
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
|
88
|
+
from reportlab.lib.styles import getSampleStyleSheet
|
|
89
|
+
|
|
90
|
+
doc = SimpleDocTemplate("report.pdf", pagesize=letter)
|
|
91
|
+
styles = getSampleStyleSheet()
|
|
92
|
+
story = []
|
|
93
|
+
|
|
94
|
+
# Add content
|
|
95
|
+
title = Paragraph("Report Title", styles['Title'])
|
|
96
|
+
story.append(title)
|
|
97
|
+
story.append(Spacer(1, 12))
|
|
98
|
+
|
|
99
|
+
body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
|
|
100
|
+
story.append(body)
|
|
101
|
+
story.append(PageBreak())
|
|
102
|
+
|
|
103
|
+
# Page 2
|
|
104
|
+
story.append(Paragraph("Page 2", styles['Heading1']))
|
|
105
|
+
story.append(Paragraph("Content for page 2", styles['Normal']))
|
|
106
|
+
|
|
107
|
+
# Build PDF
|
|
108
|
+
doc.build(story)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Subscripts and Superscripts
|
|
112
|
+
|
|
113
|
+
**IMPORTANT**: Never use Unicode subscript/superscript characters (₀₁₂₃₄₅₆₇₈₉, ⁰¹²³⁴⁵⁶⁷⁸⁹) in ReportLab PDFs. The built-in fonts do not include these glyphs, causing them to render as solid black boxes.
|
|
114
|
+
|
|
115
|
+
Instead, use ReportLab's XML markup tags in Paragraph objects:
|
|
116
|
+
```python
|
|
117
|
+
from reportlab.platypus import Paragraph
|
|
118
|
+
from reportlab.lib.styles import getSampleStyleSheet
|
|
119
|
+
|
|
120
|
+
styles = getSampleStyleSheet()
|
|
121
|
+
|
|
122
|
+
# Subscripts: use <sub> tag
|
|
123
|
+
chemical = Paragraph("H<sub>2</sub>O", styles['Normal'])
|
|
124
|
+
|
|
125
|
+
# Superscripts: use <super> tag
|
|
126
|
+
squared = Paragraph("x<super>2</super> + y<super>2</super>", styles['Normal'])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For canvas-drawn text (not Paragraph objects), manually adjust font the size and position rather than using Unicode subscripts/superscripts.
|
|
130
|
+
|
|
131
|
+
### reportlab Advanced Features
|
|
132
|
+
|
|
133
|
+
#### Create Professional Reports with Tables
|
|
134
|
+
```python
|
|
135
|
+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
|
|
136
|
+
from reportlab.lib.styles import getSampleStyleSheet
|
|
137
|
+
from reportlab.lib import colors
|
|
138
|
+
|
|
139
|
+
# Sample data
|
|
140
|
+
data = [
|
|
141
|
+
['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
|
|
142
|
+
['Widgets', '120', '135', '142', '158'],
|
|
143
|
+
['Gadgets', '85', '92', '98', '105']
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Create PDF with table
|
|
147
|
+
doc = SimpleDocTemplate("report.pdf")
|
|
148
|
+
elements = []
|
|
149
|
+
|
|
150
|
+
# Add title
|
|
151
|
+
styles = getSampleStyleSheet()
|
|
152
|
+
title = Paragraph("Quarterly Sales Report", styles['Title'])
|
|
153
|
+
elements.append(title)
|
|
154
|
+
|
|
155
|
+
# Add table with advanced styling
|
|
156
|
+
table = Table(data)
|
|
157
|
+
table.setStyle(TableStyle([
|
|
158
|
+
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
|
159
|
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
|
160
|
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
161
|
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
162
|
+
('FONTSIZE', (0, 0), (-1, 0), 14),
|
|
163
|
+
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
|
164
|
+
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
|
165
|
+
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
|
166
|
+
]))
|
|
167
|
+
elements.append(table)
|
|
168
|
+
|
|
169
|
+
doc.build(elements)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## pdf-lib (MIT License) - Create Complex PDFs from Scratch
|
|
173
|
+
|
|
174
|
+
pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
|
178
|
+
import fs from 'fs';
|
|
179
|
+
|
|
180
|
+
async function createPDF() {
|
|
181
|
+
const pdfDoc = await PDFDocument.create();
|
|
182
|
+
|
|
183
|
+
// Add fonts
|
|
184
|
+
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
185
|
+
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
186
|
+
|
|
187
|
+
// Add page
|
|
188
|
+
const page = pdfDoc.addPage([595, 842]); // A4 size
|
|
189
|
+
const { width, height } = page.getSize();
|
|
190
|
+
|
|
191
|
+
// Add text with styling
|
|
192
|
+
page.drawText('Invoice #12345', {
|
|
193
|
+
x: 50,
|
|
194
|
+
y: height - 50,
|
|
195
|
+
size: 18,
|
|
196
|
+
font: helveticaBold,
|
|
197
|
+
color: rgb(0.2, 0.2, 0.8)
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Add rectangle (header background)
|
|
201
|
+
page.drawRectangle({
|
|
202
|
+
x: 40,
|
|
203
|
+
y: height - 100,
|
|
204
|
+
width: width - 80,
|
|
205
|
+
height: 30,
|
|
206
|
+
color: rgb(0.9, 0.9, 0.9)
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Add table-like content
|
|
210
|
+
const items = [
|
|
211
|
+
['Item', 'Qty', 'Price', 'Total'],
|
|
212
|
+
['Widget', '2', '$50', '$100'],
|
|
213
|
+
['Gadget', '1', '$75', '$75']
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
let yPos = height - 150;
|
|
217
|
+
items.forEach(row => {
|
|
218
|
+
let xPos = 50;
|
|
219
|
+
row.forEach(cell => {
|
|
220
|
+
page.drawText(cell, {
|
|
221
|
+
x: xPos,
|
|
222
|
+
y: yPos,
|
|
223
|
+
size: 12,
|
|
224
|
+
font: helveticaFont
|
|
225
|
+
});
|
|
226
|
+
xPos += 120;
|
|
227
|
+
});
|
|
228
|
+
yPos -= 25;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const pdfBytes = await pdfDoc.save();
|
|
232
|
+
fs.writeFileSync('created.pdf', pdfBytes);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
<!-- module: 02-extraction.md -->
|
|
238
|
+
|
|
239
|
+
# Extracting Text, Tables, and Metadata
|
|
240
|
+
|
|
241
|
+
Extracting text, tables, coordinates, metadata, and annotations from PDFs (pdfplumber, pdftotext, pypdf, pdfjs-dist).
|
|
242
|
+
|
|
243
|
+
## pdfplumber - Text and Table Extraction
|
|
244
|
+
|
|
245
|
+
### Extract Text with Layout
|
|
246
|
+
```python
|
|
247
|
+
import pdfplumber
|
|
248
|
+
|
|
249
|
+
with pdfplumber.open("document.pdf") as pdf:
|
|
250
|
+
for page in pdf.pages:
|
|
251
|
+
text = page.extract_text()
|
|
252
|
+
print(text)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Extract Tables
|
|
256
|
+
```python
|
|
257
|
+
with pdfplumber.open("document.pdf") as pdf:
|
|
258
|
+
for i, page in enumerate(pdf.pages):
|
|
259
|
+
tables = page.extract_tables()
|
|
260
|
+
for j, table in enumerate(tables):
|
|
261
|
+
print(f"Table {j+1} on page {i+1}:")
|
|
262
|
+
for row in table:
|
|
263
|
+
print(row)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Advanced Table Extraction
|
|
267
|
+
```python
|
|
268
|
+
import pandas as pd
|
|
269
|
+
|
|
270
|
+
with pdfplumber.open("document.pdf") as pdf:
|
|
271
|
+
all_tables = []
|
|
272
|
+
for page in pdf.pages:
|
|
273
|
+
tables = page.extract_tables()
|
|
274
|
+
for table in tables:
|
|
275
|
+
if table: # Check if table is not empty
|
|
276
|
+
df = pd.DataFrame(table[1:], columns=table[0])
|
|
277
|
+
all_tables.append(df)
|
|
278
|
+
|
|
279
|
+
# Combine all tables
|
|
280
|
+
if all_tables:
|
|
281
|
+
combined_df = pd.concat(all_tables, ignore_index=True)
|
|
282
|
+
combined_df.to_excel("extracted_tables.xlsx", index=False)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## pypdf - Extract Metadata
|
|
286
|
+
```python
|
|
287
|
+
reader = PdfReader("document.pdf")
|
|
288
|
+
meta = reader.metadata
|
|
289
|
+
print(f"Title: {meta.title}")
|
|
290
|
+
print(f"Author: {meta.author}")
|
|
291
|
+
print(f"Subject: {meta.subject}")
|
|
292
|
+
print(f"Creator: {meta.creator}")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## pdftotext (poppler-utils)
|
|
296
|
+
```bash
|
|
297
|
+
# Extract text
|
|
298
|
+
pdftotext input.pdf output.txt
|
|
299
|
+
|
|
300
|
+
# Extract text preserving layout
|
|
301
|
+
pdftotext -layout input.pdf output.txt
|
|
302
|
+
|
|
303
|
+
# Extract specific pages
|
|
304
|
+
pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Advanced: pdfplumber
|
|
308
|
+
|
|
309
|
+
### Extract Text with Precise Coordinates
|
|
310
|
+
```python
|
|
311
|
+
import pdfplumber
|
|
312
|
+
|
|
313
|
+
with pdfplumber.open("document.pdf") as pdf:
|
|
314
|
+
page = pdf.pages[0]
|
|
315
|
+
|
|
316
|
+
# Extract all text with coordinates
|
|
317
|
+
chars = page.chars
|
|
318
|
+
for char in chars[:10]: # First 10 characters
|
|
319
|
+
print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}")
|
|
320
|
+
|
|
321
|
+
# Extract text by bounding box (left, top, right, bottom)
|
|
322
|
+
bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Advanced Table Extraction with Custom Settings
|
|
326
|
+
```python
|
|
327
|
+
import pdfplumber
|
|
328
|
+
import pandas as pd
|
|
329
|
+
|
|
330
|
+
with pdfplumber.open("complex_table.pdf") as pdf:
|
|
331
|
+
page = pdf.pages[0]
|
|
332
|
+
|
|
333
|
+
# Extract tables with custom settings for complex layouts
|
|
334
|
+
table_settings = {
|
|
335
|
+
"vertical_strategy": "lines",
|
|
336
|
+
"horizontal_strategy": "lines",
|
|
337
|
+
"snap_tolerance": 3,
|
|
338
|
+
"intersection_tolerance": 15
|
|
339
|
+
}
|
|
340
|
+
tables = page.extract_tables(table_settings)
|
|
341
|
+
|
|
342
|
+
# Visual debugging for table extraction
|
|
343
|
+
img = page.to_image(resolution=150)
|
|
344
|
+
img.save("debug_layout.png")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Advanced: poppler-utils
|
|
348
|
+
|
|
349
|
+
### Extract Text with Bounding Box Coordinates
|
|
350
|
+
```bash
|
|
351
|
+
# Extract text with bounding box coordinates (essential for structured data)
|
|
352
|
+
pdftotext -bbox-layout document.pdf output.xml
|
|
353
|
+
|
|
354
|
+
# The XML output contains precise coordinates for each text element
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## pdfjs-dist (Apache License)
|
|
358
|
+
|
|
359
|
+
PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
|
|
360
|
+
|
|
361
|
+
### Extract Text with Coordinates
|
|
362
|
+
```javascript
|
|
363
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
364
|
+
|
|
365
|
+
async function extractText() {
|
|
366
|
+
const loadingTask = pdfjsLib.getDocument('document.pdf');
|
|
367
|
+
const pdf = await loadingTask.promise;
|
|
368
|
+
|
|
369
|
+
let fullText = '';
|
|
370
|
+
|
|
371
|
+
// Extract text from all pages
|
|
372
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
373
|
+
const page = await pdf.getPage(i);
|
|
374
|
+
const textContent = await page.getTextContent();
|
|
375
|
+
|
|
376
|
+
const pageText = textContent.items
|
|
377
|
+
.map(item => item.str)
|
|
378
|
+
.join(' ');
|
|
379
|
+
|
|
380
|
+
fullText += `\n--- Page ${i} ---\n${pageText}`;
|
|
381
|
+
|
|
382
|
+
// Get text with coordinates for advanced processing
|
|
383
|
+
const textWithCoords = textContent.items.map(item => ({
|
|
384
|
+
text: item.str,
|
|
385
|
+
x: item.transform[4],
|
|
386
|
+
y: item.transform[5],
|
|
387
|
+
width: item.width,
|
|
388
|
+
height: item.height
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(fullText);
|
|
393
|
+
return fullText;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Extract Annotations and Forms
|
|
398
|
+
```javascript
|
|
399
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
400
|
+
|
|
401
|
+
async function extractAnnotations() {
|
|
402
|
+
const loadingTask = pdfjsLib.getDocument('annotated.pdf');
|
|
403
|
+
const pdf = await loadingTask.promise;
|
|
404
|
+
|
|
405
|
+
for (let i = 1; i <= pdf.numPages; i++) {
|
|
406
|
+
const page = await pdf.getPage(i);
|
|
407
|
+
const annotations = await page.getAnnotations();
|
|
408
|
+
|
|
409
|
+
annotations.forEach(annotation => {
|
|
410
|
+
console.log(`Annotation type: ${annotation.subtype}`);
|
|
411
|
+
console.log(`Content: ${annotation.contents}`);
|
|
412
|
+
console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
<!-- module: 03-merge-split-manipulate.md -->
|
|
420
|
+
|
|
421
|
+
# Merging, Splitting, Rotating, and Manipulating Pages
|
|
422
|
+
|
|
423
|
+
Combining, splitting, rotating, watermarking, cropping, optimizing, and batch-processing PDFs (pypdf, qpdf, pdftk, pdf-lib).
|
|
424
|
+
|
|
425
|
+
## pypdf - Basic Operations
|
|
426
|
+
|
|
427
|
+
### Merge PDFs
|
|
428
|
+
```python
|
|
429
|
+
from pypdf import PdfWriter, PdfReader
|
|
430
|
+
|
|
431
|
+
writer = PdfWriter()
|
|
432
|
+
for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
|
|
433
|
+
reader = PdfReader(pdf_file)
|
|
434
|
+
for page in reader.pages:
|
|
435
|
+
writer.add_page(page)
|
|
436
|
+
|
|
437
|
+
with open("merged.pdf", "wb") as output:
|
|
438
|
+
writer.write(output)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Split PDF
|
|
442
|
+
```python
|
|
443
|
+
reader = PdfReader("input.pdf")
|
|
444
|
+
for i, page in enumerate(reader.pages):
|
|
445
|
+
writer = PdfWriter()
|
|
446
|
+
writer.add_page(page)
|
|
447
|
+
with open(f"page_{i+1}.pdf", "wb") as output:
|
|
448
|
+
writer.write(output)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Rotate Pages
|
|
452
|
+
```python
|
|
453
|
+
reader = PdfReader("input.pdf")
|
|
454
|
+
writer = PdfWriter()
|
|
455
|
+
|
|
456
|
+
page = reader.pages[0]
|
|
457
|
+
page.rotate(90) # Rotate 90 degrees clockwise
|
|
458
|
+
writer.add_page(page)
|
|
459
|
+
|
|
460
|
+
with open("rotated.pdf", "wb") as output:
|
|
461
|
+
writer.write(output)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## qpdf
|
|
465
|
+
```bash
|
|
466
|
+
# Merge PDFs
|
|
467
|
+
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
|
|
468
|
+
|
|
469
|
+
# Split pages
|
|
470
|
+
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
|
|
471
|
+
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf
|
|
472
|
+
|
|
473
|
+
# Rotate pages
|
|
474
|
+
qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## pdftk (if available)
|
|
478
|
+
```bash
|
|
479
|
+
# Merge
|
|
480
|
+
pdftk file1.pdf file2.pdf cat output merged.pdf
|
|
481
|
+
|
|
482
|
+
# Split
|
|
483
|
+
pdftk input.pdf burst
|
|
484
|
+
|
|
485
|
+
# Rotate
|
|
486
|
+
pdftk input.pdf rotate 1east output rotated.pdf
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Add Watermark
|
|
490
|
+
```python
|
|
491
|
+
from pypdf import PdfReader, PdfWriter
|
|
492
|
+
|
|
493
|
+
# Create watermark (or load existing)
|
|
494
|
+
watermark = PdfReader("watermark.pdf").pages[0]
|
|
495
|
+
|
|
496
|
+
# Apply to all pages
|
|
497
|
+
reader = PdfReader("document.pdf")
|
|
498
|
+
writer = PdfWriter()
|
|
499
|
+
|
|
500
|
+
for page in reader.pages:
|
|
501
|
+
page.merge_page(watermark)
|
|
502
|
+
writer.add_page(page)
|
|
503
|
+
|
|
504
|
+
with open("watermarked.pdf", "wb") as output:
|
|
505
|
+
writer.write(output)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## pdf-lib (MIT License)
|
|
509
|
+
|
|
510
|
+
### Load and Manipulate Existing PDF
|
|
511
|
+
```javascript
|
|
512
|
+
import { PDFDocument } from 'pdf-lib';
|
|
513
|
+
import fs from 'fs';
|
|
514
|
+
|
|
515
|
+
async function manipulatePDF() {
|
|
516
|
+
// Load existing PDF
|
|
517
|
+
const existingPdfBytes = fs.readFileSync('input.pdf');
|
|
518
|
+
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
|
519
|
+
|
|
520
|
+
// Get page count
|
|
521
|
+
const pageCount = pdfDoc.getPageCount();
|
|
522
|
+
console.log(`Document has ${pageCount} pages`);
|
|
523
|
+
|
|
524
|
+
// Add new page
|
|
525
|
+
const newPage = pdfDoc.addPage([600, 400]);
|
|
526
|
+
newPage.drawText('Added by pdf-lib', {
|
|
527
|
+
x: 100,
|
|
528
|
+
y: 300,
|
|
529
|
+
size: 16
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Save modified PDF
|
|
533
|
+
const pdfBytes = await pdfDoc.save();
|
|
534
|
+
fs.writeFileSync('modified.pdf', pdfBytes);
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Advanced Merge and Split Operations
|
|
539
|
+
```javascript
|
|
540
|
+
import { PDFDocument } from 'pdf-lib';
|
|
541
|
+
import fs from 'fs';
|
|
542
|
+
|
|
543
|
+
async function mergePDFs() {
|
|
544
|
+
// Create new document
|
|
545
|
+
const mergedPdf = await PDFDocument.create();
|
|
546
|
+
|
|
547
|
+
// Load source PDFs
|
|
548
|
+
const pdf1Bytes = fs.readFileSync('doc1.pdf');
|
|
549
|
+
const pdf2Bytes = fs.readFileSync('doc2.pdf');
|
|
550
|
+
|
|
551
|
+
const pdf1 = await PDFDocument.load(pdf1Bytes);
|
|
552
|
+
const pdf2 = await PDFDocument.load(pdf2Bytes);
|
|
553
|
+
|
|
554
|
+
// Copy pages from first PDF
|
|
555
|
+
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
|
|
556
|
+
pdf1Pages.forEach(page => mergedPdf.addPage(page));
|
|
557
|
+
|
|
558
|
+
// Copy specific pages from second PDF (pages 0, 2, 4)
|
|
559
|
+
const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
|
|
560
|
+
pdf2Pages.forEach(page => mergedPdf.addPage(page));
|
|
561
|
+
|
|
562
|
+
const mergedPdfBytes = await mergedPdf.save();
|
|
563
|
+
fs.writeFileSync('merged.pdf', mergedPdfBytes);
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## qpdf Advanced Features
|
|
568
|
+
|
|
569
|
+
### Complex Page Manipulation
|
|
570
|
+
```bash
|
|
571
|
+
# Split PDF into groups of pages
|
|
572
|
+
qpdf --split-pages=3 input.pdf output_group_%02d.pdf
|
|
573
|
+
|
|
574
|
+
# Extract specific pages with complex ranges
|
|
575
|
+
qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
|
|
576
|
+
|
|
577
|
+
# Merge specific pages from multiple PDFs
|
|
578
|
+
qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### PDF Optimization and Repair
|
|
582
|
+
```bash
|
|
583
|
+
# Optimize PDF for web (linearize for streaming)
|
|
584
|
+
qpdf --linearize input.pdf optimized.pdf
|
|
585
|
+
|
|
586
|
+
# Remove unused objects and compress
|
|
587
|
+
qpdf --optimize-level=all input.pdf compressed.pdf
|
|
588
|
+
|
|
589
|
+
# Attempt to repair corrupted PDF structure
|
|
590
|
+
qpdf --check input.pdf
|
|
591
|
+
qpdf --fix-qdf damaged.pdf repaired.pdf
|
|
592
|
+
|
|
593
|
+
# Show detailed PDF structure for debugging
|
|
594
|
+
qpdf --show-all-pages input.pdf > structure.txt
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Advanced PDF Cropping
|
|
598
|
+
```python
|
|
599
|
+
from pypdf import PdfWriter, PdfReader
|
|
600
|
+
|
|
601
|
+
reader = PdfReader("input.pdf")
|
|
602
|
+
writer = PdfWriter()
|
|
603
|
+
|
|
604
|
+
# Crop page (left, bottom, right, top in points)
|
|
605
|
+
page = reader.pages[0]
|
|
606
|
+
page.mediabox.left = 50
|
|
607
|
+
page.mediabox.bottom = 50
|
|
608
|
+
page.mediabox.right = 550
|
|
609
|
+
page.mediabox.top = 750
|
|
610
|
+
|
|
611
|
+
writer.add_page(page)
|
|
612
|
+
with open("cropped.pdf", "wb") as output:
|
|
613
|
+
writer.write(output)
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## Batch PDF Processing with Error Handling
|
|
617
|
+
```python
|
|
618
|
+
import os
|
|
619
|
+
import glob
|
|
620
|
+
from pypdf import PdfReader, PdfWriter
|
|
621
|
+
import logging
|
|
622
|
+
|
|
623
|
+
logging.basicConfig(level=logging.INFO)
|
|
624
|
+
logger = logging.getLogger(__name__)
|
|
625
|
+
|
|
626
|
+
def batch_process_pdfs(input_dir, operation='merge'):
|
|
627
|
+
pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
|
|
628
|
+
|
|
629
|
+
if operation == 'merge':
|
|
630
|
+
writer = PdfWriter()
|
|
631
|
+
for pdf_file in pdf_files:
|
|
632
|
+
try:
|
|
633
|
+
reader = PdfReader(pdf_file)
|
|
634
|
+
for page in reader.pages:
|
|
635
|
+
writer.add_page(page)
|
|
636
|
+
logger.info(f"Processed: {pdf_file}")
|
|
637
|
+
except Exception as e:
|
|
638
|
+
logger.error(f"Failed to process {pdf_file}: {e}")
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
with open("batch_merged.pdf", "wb") as output:
|
|
642
|
+
writer.write(output)
|
|
643
|
+
|
|
644
|
+
elif operation == 'extract_text':
|
|
645
|
+
for pdf_file in pdf_files:
|
|
646
|
+
try:
|
|
647
|
+
reader = PdfReader(pdf_file)
|
|
648
|
+
text = ""
|
|
649
|
+
for page in reader.pages:
|
|
650
|
+
text += page.extract_text()
|
|
651
|
+
|
|
652
|
+
output_file = pdf_file.replace('.pdf', '.txt')
|
|
653
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
654
|
+
f.write(text)
|
|
655
|
+
logger.info(f"Extracted text from: {pdf_file}")
|
|
656
|
+
|
|
657
|
+
except Exception as e:
|
|
658
|
+
logger.error(f"Failed to extract text from {pdf_file}: {e}")
|
|
659
|
+
continue
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
<!-- module: 04-images-ocr.md -->
|
|
664
|
+
|
|
665
|
+
# Images, Rendering, and OCR
|
|
666
|
+
|
|
667
|
+
Extracting images, rendering pages to images, extracting figures, and OCR on scanned PDFs (pdfimages, pypdfium2, pdftoppm, pdfjs-dist, pytesseract).
|
|
668
|
+
|
|
669
|
+
## Extract Text from Scanned PDFs (OCR)
|
|
670
|
+
```python
|
|
671
|
+
# Requires: pip install pytesseract pdf2image
|
|
672
|
+
import pytesseract
|
|
673
|
+
from pdf2image import convert_from_path
|
|
674
|
+
|
|
675
|
+
# Convert PDF to images
|
|
676
|
+
images = convert_from_path('scanned.pdf')
|
|
677
|
+
|
|
678
|
+
# OCR each page
|
|
679
|
+
text = ""
|
|
680
|
+
for i, image in enumerate(images):
|
|
681
|
+
text += f"Page {i+1}:\n"
|
|
682
|
+
text += pytesseract.image_to_string(image)
|
|
683
|
+
text += "\n\n"
|
|
684
|
+
|
|
685
|
+
print(text)
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Extract Images
|
|
689
|
+
```bash
|
|
690
|
+
# Using pdfimages (poppler-utils)
|
|
691
|
+
pdfimages -j input.pdf output_prefix
|
|
692
|
+
|
|
693
|
+
# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## pypdfium2 Library (Apache/BSD License)
|
|
697
|
+
|
|
698
|
+
### Overview
|
|
699
|
+
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.
|
|
700
|
+
|
|
701
|
+
### Render PDF to Images
|
|
702
|
+
```python
|
|
703
|
+
import pypdfium2 as pdfium
|
|
704
|
+
from PIL import Image
|
|
705
|
+
|
|
706
|
+
# Load PDF
|
|
707
|
+
pdf = pdfium.PdfDocument("document.pdf")
|
|
708
|
+
|
|
709
|
+
# Render page to image
|
|
710
|
+
page = pdf[0] # First page
|
|
711
|
+
bitmap = page.render(
|
|
712
|
+
scale=2.0, # Higher resolution
|
|
713
|
+
rotation=0 # No rotation
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Convert to PIL Image
|
|
717
|
+
img = bitmap.to_pil()
|
|
718
|
+
img.save("page_1.png", "PNG")
|
|
719
|
+
|
|
720
|
+
# Process multiple pages
|
|
721
|
+
for i, page in enumerate(pdf):
|
|
722
|
+
bitmap = page.render(scale=1.5)
|
|
723
|
+
img = bitmap.to_pil()
|
|
724
|
+
img.save(f"page_{i+1}.jpg", "JPEG", quality=90)
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Extract Text with pypdfium2
|
|
728
|
+
```python
|
|
729
|
+
import pypdfium2 as pdfium
|
|
730
|
+
|
|
731
|
+
pdf = pdfium.PdfDocument("document.pdf")
|
|
732
|
+
for i, page in enumerate(pdf):
|
|
733
|
+
text = page.get_text()
|
|
734
|
+
print(f"Page {i+1} text length: {len(text)} chars")
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## pdfjs-dist (Apache License) - Rendering
|
|
738
|
+
|
|
739
|
+
PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
|
|
740
|
+
|
|
741
|
+
### Basic PDF Loading and Rendering
|
|
742
|
+
```javascript
|
|
743
|
+
import * as pdfjsLib from 'pdfjs-dist';
|
|
744
|
+
|
|
745
|
+
// Configure worker (important for performance)
|
|
746
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js';
|
|
747
|
+
|
|
748
|
+
async function renderPDF() {
|
|
749
|
+
// Load PDF
|
|
750
|
+
const loadingTask = pdfjsLib.getDocument('document.pdf');
|
|
751
|
+
const pdf = await loadingTask.promise;
|
|
752
|
+
|
|
753
|
+
console.log(`Loaded PDF with ${pdf.numPages} pages`);
|
|
754
|
+
|
|
755
|
+
// Get first page
|
|
756
|
+
const page = await pdf.getPage(1);
|
|
757
|
+
const viewport = page.getViewport({ scale: 1.5 });
|
|
758
|
+
|
|
759
|
+
// Render to canvas
|
|
760
|
+
const canvas = document.createElement('canvas');
|
|
761
|
+
const context = canvas.getContext('2d');
|
|
762
|
+
canvas.height = viewport.height;
|
|
763
|
+
canvas.width = viewport.width;
|
|
764
|
+
|
|
765
|
+
const renderContext = {
|
|
766
|
+
canvasContext: context,
|
|
767
|
+
viewport: viewport
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
await page.render(renderContext).promise;
|
|
771
|
+
document.body.appendChild(canvas);
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## Advanced Image Conversion (poppler-utils)
|
|
776
|
+
```bash
|
|
777
|
+
# Convert to PNG images with specific resolution
|
|
778
|
+
pdftoppm -png -r 300 document.pdf output_prefix
|
|
779
|
+
|
|
780
|
+
# Convert specific page range with high resolution
|
|
781
|
+
pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
|
|
782
|
+
|
|
783
|
+
# Convert to JPEG with quality setting
|
|
784
|
+
pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## Extract Embedded Images (poppler-utils)
|
|
788
|
+
```bash
|
|
789
|
+
# Extract all embedded images with metadata
|
|
790
|
+
pdfimages -j -p document.pdf page_images
|
|
791
|
+
|
|
792
|
+
# List image info without extracting
|
|
793
|
+
pdfimages -list document.pdf
|
|
794
|
+
|
|
795
|
+
# Extract images in their original format
|
|
796
|
+
pdfimages -all document.pdf images/img
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## Extract Figures/Images from PDF
|
|
800
|
+
|
|
801
|
+
### Method 1: Using pdfimages (fastest)
|
|
802
|
+
```bash
|
|
803
|
+
# Extract all images with original quality
|
|
804
|
+
pdfimages -all document.pdf images/img
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Method 2: Using pypdfium2 + Image Processing
|
|
808
|
+
```python
|
|
809
|
+
import pypdfium2 as pdfium
|
|
810
|
+
from PIL import Image
|
|
811
|
+
import numpy as np
|
|
812
|
+
|
|
813
|
+
def extract_figures(pdf_path, output_dir):
|
|
814
|
+
pdf = pdfium.PdfDocument(pdf_path)
|
|
815
|
+
|
|
816
|
+
for page_num, page in enumerate(pdf):
|
|
817
|
+
# Render high-resolution page
|
|
818
|
+
bitmap = page.render(scale=3.0)
|
|
819
|
+
img = bitmap.to_pil()
|
|
820
|
+
|
|
821
|
+
# Convert to numpy for processing
|
|
822
|
+
img_array = np.array(img)
|
|
823
|
+
|
|
824
|
+
# Simple figure detection (non-white regions)
|
|
825
|
+
mask = np.any(img_array != [255, 255, 255], axis=2)
|
|
826
|
+
|
|
827
|
+
# Find contours and extract bounding boxes
|
|
828
|
+
# (This is simplified - real implementation would need more sophisticated detection)
|
|
829
|
+
|
|
830
|
+
# Save detected figures
|
|
831
|
+
# ... implementation depends on specific needs
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
<!-- module: 05-encryption.md -->
|
|
836
|
+
|
|
837
|
+
# Encryption, Passwords, and Decryption
|
|
838
|
+
|
|
839
|
+
Adding and removing password protection and setting permissions (pypdf, qpdf).
|
|
840
|
+
|
|
841
|
+
## Password Protection (pypdf)
|
|
842
|
+
```python
|
|
843
|
+
from pypdf import PdfReader, PdfWriter
|
|
844
|
+
|
|
845
|
+
reader = PdfReader("input.pdf")
|
|
846
|
+
writer = PdfWriter()
|
|
847
|
+
|
|
848
|
+
for page in reader.pages:
|
|
849
|
+
writer.add_page(page)
|
|
850
|
+
|
|
851
|
+
# Add password
|
|
852
|
+
writer.encrypt("userpassword", "ownerpassword")
|
|
853
|
+
|
|
854
|
+
with open("encrypted.pdf", "wb") as output:
|
|
855
|
+
writer.write(output)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
## Remove Password (qpdf)
|
|
859
|
+
```bash
|
|
860
|
+
# Remove password
|
|
861
|
+
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Advanced Encryption (qpdf)
|
|
865
|
+
```bash
|
|
866
|
+
# Add password protection with specific permissions
|
|
867
|
+
qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
|
|
868
|
+
|
|
869
|
+
# Check encryption status
|
|
870
|
+
qpdf --show-encryption encrypted.pdf
|
|
871
|
+
|
|
872
|
+
# Remove password protection (requires password)
|
|
873
|
+
qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
## Handling Encrypted PDFs (pypdf)
|
|
877
|
+
```python
|
|
878
|
+
# Handle password-protected PDFs
|
|
879
|
+
from pypdf import PdfReader
|
|
880
|
+
|
|
881
|
+
try:
|
|
882
|
+
reader = PdfReader("encrypted.pdf")
|
|
883
|
+
if reader.is_encrypted:
|
|
884
|
+
reader.decrypt("password")
|
|
885
|
+
except Exception as e:
|
|
886
|
+
print(f"Failed to decrypt: {e}")
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
<!-- module: 06-forms.md -->
|
|
891
|
+
|
|
892
|
+
# Filling PDF Forms
|
|
893
|
+
|
|
894
|
+
Filling both fillable (AcroForm) and non-fillable (flat/scanned) PDF forms.
|
|
895
|
+
|
|
896
|
+
> **Vendored scripts:** every `python scripts/...` command below refers to the helper scripts vendored in the source skill at `skills/sources/anthropic/pdf/scripts/` (`check_fillable_fields.py`, `extract_form_field_info.py`, `convert_pdf_to_images.py`, `fill_fillable_fields.py`, `extract_form_structure.py`, `check_bounding_boxes.py`, `fill_pdf_form_with_annotations.py`). "This file's directory" means the source skill directory.
|
|
897
|
+
|
|
898
|
+
**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
|
|
899
|
+
|
|
900
|
+
If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
|
|
901
|
+
`python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
|
|
902
|
+
|
|
903
|
+
# Fillable fields
|
|
904
|
+
If the PDF has fillable form fields:
|
|
905
|
+
- Run this script from this file's directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format:
|
|
906
|
+
```
|
|
907
|
+
[
|
|
908
|
+
{
|
|
909
|
+
"field_id": (unique ID for the field),
|
|
910
|
+
"page": (page number, 1-based),
|
|
911
|
+
"rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
|
|
912
|
+
"type": ("text", "checkbox", "radio_group", or "choice"),
|
|
913
|
+
},
|
|
914
|
+
// Checkboxes have "checked_value" and "unchecked_value" properties:
|
|
915
|
+
{
|
|
916
|
+
"field_id": (unique ID for the field),
|
|
917
|
+
"page": (page number, 1-based),
|
|
918
|
+
"type": "checkbox",
|
|
919
|
+
"checked_value": (Set the field to this value to check the checkbox),
|
|
920
|
+
"unchecked_value": (Set the field to this value to uncheck the checkbox),
|
|
921
|
+
},
|
|
922
|
+
// Radio groups have a "radio_options" list with the possible choices.
|
|
923
|
+
{
|
|
924
|
+
"field_id": (unique ID for the field),
|
|
925
|
+
"page": (page number, 1-based),
|
|
926
|
+
"type": "radio_group",
|
|
927
|
+
"radio_options": [
|
|
928
|
+
{
|
|
929
|
+
"value": (set the field to this value to select this radio option),
|
|
930
|
+
"rect": (bounding box for the radio button for this option)
|
|
931
|
+
},
|
|
932
|
+
// Other radio options
|
|
933
|
+
]
|
|
934
|
+
},
|
|
935
|
+
// Multiple choice fields have a "choice_options" list with the possible choices:
|
|
936
|
+
{
|
|
937
|
+
"field_id": (unique ID for the field),
|
|
938
|
+
"page": (page number, 1-based),
|
|
939
|
+
"type": "choice",
|
|
940
|
+
"choice_options": [
|
|
941
|
+
{
|
|
942
|
+
"value": (set the field to this value to select this option),
|
|
943
|
+
"text": (display text of the option)
|
|
944
|
+
},
|
|
945
|
+
// Other choice options
|
|
946
|
+
],
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
```
|
|
950
|
+
- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
|
|
951
|
+
`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`
|
|
952
|
+
Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
|
|
953
|
+
- Create a `field_values.json` file in this format with the values to be entered for each field:
|
|
954
|
+
```
|
|
955
|
+
[
|
|
956
|
+
{
|
|
957
|
+
"field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
|
|
958
|
+
"description": "The user's last name",
|
|
959
|
+
"page": 1, // Must match the "page" value in field_info.json
|
|
960
|
+
"value": "Simpson"
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
"field_id": "Checkbox12",
|
|
964
|
+
"description": "Checkbox to be checked if the user is 18 or over",
|
|
965
|
+
"page": 1,
|
|
966
|
+
"value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
|
|
967
|
+
},
|
|
968
|
+
// more fields
|
|
969
|
+
]
|
|
970
|
+
```
|
|
971
|
+
- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
|
|
972
|
+
`python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf>`
|
|
973
|
+
This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
|
|
974
|
+
|
|
975
|
+
# Non-fillable fields
|
|
976
|
+
If the PDF doesn't have fillable form fields, you'll add text annotations. First try to extract coordinates from the PDF structure (more accurate), then fall back to visual estimation if needed.
|
|
977
|
+
|
|
978
|
+
## Step 1: Try Structure Extraction First
|
|
979
|
+
|
|
980
|
+
Run this script to extract text labels, lines, and checkboxes with their exact PDF coordinates:
|
|
981
|
+
`python scripts/extract_form_structure.py <input.pdf> form_structure.json`
|
|
982
|
+
|
|
983
|
+
This creates a JSON file containing:
|
|
984
|
+
- **labels**: Every text element with exact coordinates (x0, top, x1, bottom in PDF points)
|
|
985
|
+
- **lines**: Horizontal lines that define row boundaries
|
|
986
|
+
- **checkboxes**: Small square rectangles that are checkboxes (with center coordinates)
|
|
987
|
+
- **row_boundaries**: Row top/bottom positions calculated from horizontal lines
|
|
988
|
+
|
|
989
|
+
**Check the results**: If `form_structure.json` has meaningful labels (text elements that correspond to form fields), use **Approach A: Structure-Based Coordinates**. If the PDF is scanned/image-based and has few or no labels, use **Approach B: Visual Estimation**.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## Approach A: Structure-Based Coordinates (Preferred)
|
|
994
|
+
|
|
995
|
+
Use this when `extract_form_structure.py` found text labels in the PDF.
|
|
996
|
+
|
|
997
|
+
### A.1: Analyze the Structure
|
|
998
|
+
|
|
999
|
+
Read form_structure.json and identify:
|
|
1000
|
+
|
|
1001
|
+
1. **Label groups**: Adjacent text elements that form a single label (e.g., "Last" + "Name")
|
|
1002
|
+
2. **Row structure**: Labels with similar `top` values are in the same row
|
|
1003
|
+
3. **Field columns**: Entry areas start after label ends (x0 = label.x1 + gap)
|
|
1004
|
+
4. **Checkboxes**: Use the checkbox coordinates directly from the structure
|
|
1005
|
+
|
|
1006
|
+
**Coordinate system**: PDF coordinates where y=0 is at TOP of page, y increases downward.
|
|
1007
|
+
|
|
1008
|
+
### A.2: Check for Missing Elements
|
|
1009
|
+
|
|
1010
|
+
The structure extraction may not detect all form elements. Common cases:
|
|
1011
|
+
- **Circular checkboxes**: Only square rectangles are detected as checkboxes
|
|
1012
|
+
- **Complex graphics**: Decorative elements or non-standard form controls
|
|
1013
|
+
- **Faded or light-colored elements**: May not be extracted
|
|
1014
|
+
|
|
1015
|
+
If you see form fields in the PDF images that aren't in form_structure.json, you'll need to use **visual analysis** for those specific fields (see "Hybrid Approach" below).
|
|
1016
|
+
|
|
1017
|
+
### A.3: Create fields.json with PDF Coordinates
|
|
1018
|
+
|
|
1019
|
+
For each field, calculate entry coordinates from the extracted structure:
|
|
1020
|
+
|
|
1021
|
+
**Text fields:**
|
|
1022
|
+
- entry x0 = label x1 + 5 (small gap after label)
|
|
1023
|
+
- entry x1 = next label's x0, or row boundary
|
|
1024
|
+
- entry top = same as label top
|
|
1025
|
+
- entry bottom = row boundary line below, or label bottom + row_height
|
|
1026
|
+
|
|
1027
|
+
**Checkboxes:**
|
|
1028
|
+
- Use the checkbox rectangle coordinates directly from form_structure.json
|
|
1029
|
+
- entry_bounding_box = [checkbox.x0, checkbox.top, checkbox.x1, checkbox.bottom]
|
|
1030
|
+
|
|
1031
|
+
Create fields.json using `pdf_width` and `pdf_height` (signals PDF coordinates):
|
|
1032
|
+
```json
|
|
1033
|
+
{
|
|
1034
|
+
"pages": [
|
|
1035
|
+
{"page_number": 1, "pdf_width": 612, "pdf_height": 792}
|
|
1036
|
+
],
|
|
1037
|
+
"form_fields": [
|
|
1038
|
+
{
|
|
1039
|
+
"page_number": 1,
|
|
1040
|
+
"description": "Last name entry field",
|
|
1041
|
+
"field_label": "Last Name",
|
|
1042
|
+
"label_bounding_box": [43, 63, 87, 73],
|
|
1043
|
+
"entry_bounding_box": [92, 63, 260, 79],
|
|
1044
|
+
"entry_text": {"text": "Smith", "font_size": 10}
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"page_number": 1,
|
|
1048
|
+
"description": "US Citizen Yes checkbox",
|
|
1049
|
+
"field_label": "Yes",
|
|
1050
|
+
"label_bounding_box": [260, 200, 280, 210],
|
|
1051
|
+
"entry_bounding_box": [285, 197, 292, 205],
|
|
1052
|
+
"entry_text": {"text": "X"}
|
|
1053
|
+
}
|
|
1054
|
+
]
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
**Important**: Use `pdf_width`/`pdf_height` and coordinates directly from form_structure.json.
|
|
1059
|
+
|
|
1060
|
+
### A.4: Validate Bounding Boxes
|
|
1061
|
+
|
|
1062
|
+
Before filling, check your bounding boxes for errors:
|
|
1063
|
+
`python scripts/check_bounding_boxes.py fields.json`
|
|
1064
|
+
|
|
1065
|
+
This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## Approach B: Visual Estimation (Fallback)
|
|
1070
|
+
|
|
1071
|
+
Use this when the PDF is scanned/image-based and structure extraction found no usable text labels (e.g., all text shows as "(cid:X)" patterns).
|
|
1072
|
+
|
|
1073
|
+
### B.1: Convert PDF to Images
|
|
1074
|
+
|
|
1075
|
+
`python scripts/convert_pdf_to_images.py <input.pdf> <images_dir/>`
|
|
1076
|
+
|
|
1077
|
+
### B.2: Initial Field Identification
|
|
1078
|
+
|
|
1079
|
+
Examine each page image to identify form sections and get **rough estimates** of field locations:
|
|
1080
|
+
- Form field labels and their approximate positions
|
|
1081
|
+
- Entry areas (lines, boxes, or blank spaces for text input)
|
|
1082
|
+
- Checkboxes and their approximate locations
|
|
1083
|
+
|
|
1084
|
+
For each field, note approximate pixel coordinates (they don't need to be precise yet).
|
|
1085
|
+
|
|
1086
|
+
### B.3: Zoom Refinement (CRITICAL for accuracy)
|
|
1087
|
+
|
|
1088
|
+
For each field, crop a region around the estimated position to refine coordinates precisely.
|
|
1089
|
+
|
|
1090
|
+
**Create a zoomed crop using ImageMagick:**
|
|
1091
|
+
```bash
|
|
1092
|
+
magick <page_image> -crop <width>x<height>+<x>+<y> +repage <crop_output.png>
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Where:
|
|
1096
|
+
- `<x>, <y>` = top-left corner of crop region (use your rough estimate minus padding)
|
|
1097
|
+
- `<width>, <height>` = size of crop region (field area plus ~50px padding on each side)
|
|
1098
|
+
|
|
1099
|
+
**Example:** To refine a "Name" field estimated around (100, 150):
|
|
1100
|
+
```bash
|
|
1101
|
+
magick images_dir/page_1.png -crop 300x80+50+120 +repage crops/name_field.png
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
(Note: if the `magick` command isn't available, try `convert` with the same arguments).
|
|
1105
|
+
|
|
1106
|
+
**Examine the cropped image** to determine precise coordinates:
|
|
1107
|
+
1. Identify the exact pixel where the entry area begins (after the label)
|
|
1108
|
+
2. Identify where the entry area ends (before next field or edge)
|
|
1109
|
+
3. Identify the top and bottom of the entry line/box
|
|
1110
|
+
|
|
1111
|
+
**Convert crop coordinates back to full image coordinates:**
|
|
1112
|
+
- full_x = crop_x + crop_offset_x
|
|
1113
|
+
- full_y = crop_y + crop_offset_y
|
|
1114
|
+
|
|
1115
|
+
Example: If the crop started at (50, 120) and the entry box starts at (52, 18) within the crop:
|
|
1116
|
+
- entry_x0 = 52 + 50 = 102
|
|
1117
|
+
- entry_top = 18 + 120 = 138
|
|
1118
|
+
|
|
1119
|
+
**Repeat for each field**, grouping nearby fields into single crops when possible.
|
|
1120
|
+
|
|
1121
|
+
### B.4: Create fields.json with Refined Coordinates
|
|
1122
|
+
|
|
1123
|
+
Create fields.json using `image_width` and `image_height` (signals image coordinates):
|
|
1124
|
+
```json
|
|
1125
|
+
{
|
|
1126
|
+
"pages": [
|
|
1127
|
+
{"page_number": 1, "image_width": 1700, "image_height": 2200}
|
|
1128
|
+
],
|
|
1129
|
+
"form_fields": [
|
|
1130
|
+
{
|
|
1131
|
+
"page_number": 1,
|
|
1132
|
+
"description": "Last name entry field",
|
|
1133
|
+
"field_label": "Last Name",
|
|
1134
|
+
"label_bounding_box": [120, 175, 242, 198],
|
|
1135
|
+
"entry_bounding_box": [255, 175, 720, 218],
|
|
1136
|
+
"entry_text": {"text": "Smith", "font_size": 10}
|
|
1137
|
+
}
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
**Important**: Use `image_width`/`image_height` and the refined pixel coordinates from the zoom analysis.
|
|
1143
|
+
|
|
1144
|
+
### B.5: Validate Bounding Boxes
|
|
1145
|
+
|
|
1146
|
+
Before filling, check your bounding boxes for errors:
|
|
1147
|
+
`python scripts/check_bounding_boxes.py fields.json`
|
|
1148
|
+
|
|
1149
|
+
This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## Hybrid Approach: Structure + Visual
|
|
1154
|
+
|
|
1155
|
+
Use this when structure extraction works for most fields but misses some elements (e.g., circular checkboxes, unusual form controls).
|
|
1156
|
+
|
|
1157
|
+
1. **Use Approach A** for fields that were detected in form_structure.json
|
|
1158
|
+
2. **Convert PDF to images** for visual analysis of missing fields
|
|
1159
|
+
3. **Use zoom refinement** (from Approach B) for the missing fields
|
|
1160
|
+
4. **Combine coordinates**: For fields from structure extraction, use `pdf_width`/`pdf_height`. For visually-estimated fields, you must convert image coordinates to PDF coordinates:
|
|
1161
|
+
- pdf_x = image_x * (pdf_width / image_width)
|
|
1162
|
+
- pdf_y = image_y * (pdf_height / image_height)
|
|
1163
|
+
5. **Use a single coordinate system** in fields.json - convert all to PDF coordinates with `pdf_width`/`pdf_height`
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1167
|
+
## Step 2: Validate Before Filling
|
|
1168
|
+
|
|
1169
|
+
**Always validate bounding boxes before filling:**
|
|
1170
|
+
`python scripts/check_bounding_boxes.py fields.json`
|
|
1171
|
+
|
|
1172
|
+
This checks for:
|
|
1173
|
+
- Intersecting bounding boxes (which would cause overlapping text)
|
|
1174
|
+
- Entry boxes that are too small for the specified font size
|
|
1175
|
+
|
|
1176
|
+
Fix any reported errors in fields.json before proceeding.
|
|
1177
|
+
|
|
1178
|
+
## Step 3: Fill the Form
|
|
1179
|
+
|
|
1180
|
+
The fill script auto-detects the coordinate system and handles conversion:
|
|
1181
|
+
`python scripts/fill_pdf_form_with_annotations.py <input.pdf> fields.json <output.pdf>`
|
|
1182
|
+
|
|
1183
|
+
## Step 4: Verify Output
|
|
1184
|
+
|
|
1185
|
+
Convert the filled PDF to images and verify text placement:
|
|
1186
|
+
`python scripts/convert_pdf_to_images.py <output.pdf> <verify_images/>`
|
|
1187
|
+
|
|
1188
|
+
If text is mispositioned:
|
|
1189
|
+
- **Approach A**: Check that you're using PDF coordinates from form_structure.json with `pdf_width`/`pdf_height`
|
|
1190
|
+
- **Approach B**: Check that image dimensions match and coordinates are accurate pixels
|
|
1191
|
+
- **Hybrid**: Ensure coordinate conversions are correct for visually-estimated fields
|
|
1192
|
+
|
|
1193
|
+
---
|
|
1194
|
+
<!-- module: 07-libraries-reference.md -->
|
|
1195
|
+
|
|
1196
|
+
# Library Guidance, Performance, Troubleshooting, and Licenses
|
|
1197
|
+
|
|
1198
|
+
Cross-cutting reference: performance optimization, troubleshooting recipes, and per-library license information. For library-specific code, see the creation, extraction, merge-split, and images-ocr minis.
|
|
1199
|
+
|
|
1200
|
+
## Performance Optimization Tips
|
|
1201
|
+
|
|
1202
|
+
### 1. For Large PDFs
|
|
1203
|
+
- Use streaming approaches instead of loading entire PDF in memory
|
|
1204
|
+
- Use `qpdf --split-pages` for splitting large files
|
|
1205
|
+
- Process pages individually with pypdfium2
|
|
1206
|
+
|
|
1207
|
+
### 2. For Text Extraction
|
|
1208
|
+
- `pdftotext -bbox-layout` is fastest for plain text extraction
|
|
1209
|
+
- Use pdfplumber for structured data and tables
|
|
1210
|
+
- Avoid `pypdf.extract_text()` for very large documents
|
|
1211
|
+
|
|
1212
|
+
### 3. For Image Extraction
|
|
1213
|
+
- `pdfimages` is much faster than rendering pages
|
|
1214
|
+
- Use low resolution for previews, high resolution for final output
|
|
1215
|
+
|
|
1216
|
+
### 4. For Form Filling
|
|
1217
|
+
- pdf-lib maintains form structure better than most alternatives
|
|
1218
|
+
- Pre-validate form fields before processing
|
|
1219
|
+
|
|
1220
|
+
### 5. Memory Management
|
|
1221
|
+
```python
|
|
1222
|
+
# Process PDFs in chunks
|
|
1223
|
+
def process_large_pdf(pdf_path, chunk_size=10):
|
|
1224
|
+
reader = PdfReader(pdf_path)
|
|
1225
|
+
total_pages = len(reader.pages)
|
|
1226
|
+
|
|
1227
|
+
for start_idx in range(0, total_pages, chunk_size):
|
|
1228
|
+
end_idx = min(start_idx + chunk_size, total_pages)
|
|
1229
|
+
writer = PdfWriter()
|
|
1230
|
+
|
|
1231
|
+
for i in range(start_idx, end_idx):
|
|
1232
|
+
writer.add_page(reader.pages[i])
|
|
1233
|
+
|
|
1234
|
+
# Process chunk
|
|
1235
|
+
with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
|
|
1236
|
+
writer.write(output)
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
## Troubleshooting Common Issues
|
|
1240
|
+
|
|
1241
|
+
### Encrypted PDFs
|
|
1242
|
+
```python
|
|
1243
|
+
# Handle password-protected PDFs
|
|
1244
|
+
from pypdf import PdfReader
|
|
1245
|
+
|
|
1246
|
+
try:
|
|
1247
|
+
reader = PdfReader("encrypted.pdf")
|
|
1248
|
+
if reader.is_encrypted:
|
|
1249
|
+
reader.decrypt("password")
|
|
1250
|
+
except Exception as e:
|
|
1251
|
+
print(f"Failed to decrypt: {e}")
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
### Corrupted PDFs
|
|
1255
|
+
```bash
|
|
1256
|
+
# Use qpdf to repair
|
|
1257
|
+
qpdf --check corrupted.pdf
|
|
1258
|
+
qpdf --replace-input corrupted.pdf
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
### Text Extraction Issues
|
|
1262
|
+
```python
|
|
1263
|
+
# Fallback to OCR for scanned PDFs
|
|
1264
|
+
import pytesseract
|
|
1265
|
+
from pdf2image import convert_from_path
|
|
1266
|
+
|
|
1267
|
+
def extract_text_with_ocr(pdf_path):
|
|
1268
|
+
images = convert_from_path(pdf_path)
|
|
1269
|
+
text = ""
|
|
1270
|
+
for i, image in enumerate(images):
|
|
1271
|
+
text += pytesseract.image_to_string(image)
|
|
1272
|
+
return text
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
## License Information
|
|
1276
|
+
|
|
1277
|
+
- **pypdf**: BSD License
|
|
1278
|
+
- **pdfplumber**: MIT License
|
|
1279
|
+
- **pypdfium2**: Apache/BSD License
|
|
1280
|
+
- **reportlab**: BSD License
|
|
1281
|
+
- **poppler-utils**: GPL-2 License
|
|
1282
|
+
- **qpdf**: Apache License
|
|
1283
|
+
- **pdf-lib**: MIT License
|
|
1284
|
+
- **pdfjs-dist**: Apache License
|