Create, read, edit, analyze PDFs — forms, OCR, merge, split, convert, watermark, encryption, image extraction. Any PDF/.pdf mention. Not Word or spreadsheets.
Resources
2Install
npx skillscat add darellchua2/civiltekk-skills/pdf-specialist-skill Install via the SkillsCat registry.
What I do
- Extract text and tables (pdfplumber /
pdftotext); merge, split, rotate, watermark, encrypt (pypdf / qpdf) - Fill forms — fillable AcroForm fields via pypdf; non-fillable via text annotations with validated coordinates
- Create PDFs from scratch (reportlab); convert pages to/from images (poppler-utils); OCR scanned PDFs (pytesseract)
Routing: tier 4 of the office-doc extraction ladder — structured PDF data (forms, fillable fields, OCR-as-purpose) escalates here after markitdown/docling/image-analyzer prove insufficient.
Use for: any PDF/.pdf mention.
NOT for: Word documents (.docx → docx-creation-skill), spreadsheets (Excel/CSV), coding unrelated to PDFs.
Prerequisites
pip install pypdf pdfplumber reportlab pdf2image pytesseract · poppler-utils + qpdf (sudo apt-get install poppler-utils qpdf; macOS brew install poppler qpdf; Windows winget install or WSL) · LibreOffice via scripts/soffice.py (auto-configured on first run)
Tool chain — house rules
| Task | Tool | House rule |
|---|---|---|
| Merge/split/rotate/metadata/encrypt | pypdf (or qpdf CLI) | pypdf page indices are 0-based |
| Text/table extraction | pdfplumber | preserves layout; coordinates (x0, top, x1, bottom) with y=0 at TOP of page |
| Create from scratch | reportlab | platypus flowables, not raw canvas, for multi-page docs |
| Batch CLI ops | qpdf, poppler-utils | pdftotext -layout preserves layout; pdfimages beats page rendering for image extraction |
| OCR | pytesseract + pdf2image | first resort when extraction returns garbage, (cid:X), or empty text |
| Forms | house scripts below | never fill with unvalidated coordinates |
Per-task code recipes, JSON schemas, and CLI option catalogs: `reference.md`.
House scripts
python scripts/check_fillable_fields.py <file.pdf> # branch point for form filling
python scripts/extract_form_field_info.py <in.pdf> <field_info.json> # fillable-PDF field inventory
python scripts/extract_form_structure.py <in.pdf> form_structure.json # non-fillable structure (labels/lines/boxes)
python scripts/convert_pdf_to_images.py <file.pdf> <outdir/> # also the verification pass
python scripts/check_bounding_boxes.py fields.json # validate boxes BEFORE filling
python scripts/fill_fillable_fields.py <in.pdf> <values.json> <out.pdf>
python scripts/fill_pdf_form_with_annotations.py <in.pdf> fields.json <out.pdf>
python scripts/soffice.py --headless --convert-to pdf document.docx # LibreOffice conversionsReading / extraction workflow
- Try
pdftotext -layout(fastest plain text) or pdfplumber (coordinates, tables). - Garbage /
(cid:X)/ empty output → scanned PDF → OCR via pytesseract + pdf2image. - Tables → pdfplumber
extract_tables()→ DataFrames; adjustsnap_tolerance/intersection_tolerancefor complex layouts.
Gotchas: pypdf extract_text() loses layout — prefer pdfplumber when structure matters. Encrypted: reader.is_encrypted → reader.decrypt(pw), or qpdf --password=… --decrypt. Corrupted: qpdf --check / qpdf --fix-qdf. Large files: process pages individually. Code patterns: `reference.md`.
Creation workflow (reportlab)
- Use platypus (
SimpleDocTemplate+Paragraphstory) for anything multi-page; rawcanvasonly for one-off drawing. - Never Unicode sub/superscript characters (₀₁₂…, ⁰¹²…) — built-in fonts lack the glyphs and render as solid black boxes. Use
<sub>/<super>markup tags insideParagraph; for canvas-drawn text, adjust font size and position manually.
Setup, styles, and multi-page patterns: `reference.md`.
Form filling — complete these steps in order, do not skip ahead
python scripts/check_fillable_fields.py <file.pdf>— its output branches the workflow:- Fillable → 2A · Non-fillable → 2B
2A Fillable fields
extract_form_field_info.py→field_info.json:field_id, 1-basedpage,rect([left, bottom, right, top]),type(text/checkbox/radio_group/choice); checkboxes carrychecked_value/unchecked_value, radiosradio_options, choiceschoice_options.convert_pdf_to_images.py→ analyze images to determine each field's purpose (convert PDF rects to image coords as needed).- Author
field_values.json—field_idandpagemust match field_info exactly; checkboxes usechecked_value, radios one ofradio_optionsvalues. Schema: reference.md. fill_fillable_fields.py— fix any reported field errors and re-run.
2B Non-fillable (text annotations)
extract_form_structure.py→ labels (with coordinates), lines, checkboxes, row_boundaries.- Meaningful labels → Approach A (structure-based, preferred): entry x0 = label x1 + 5; entry x1 = next label's x0 or row boundary; top = label top; bottom = row boundary line below. Use
pdf_width/pdf_heightand coordinates directly from form_structure.json. - Scanned / no usable labels → Approach B (visual estimation): convert to images, identify fields roughly, then zoom-refine —
magick page.png -crop <W>x<H>+<X>+<Y> +repage crop.png(~50px padding; useconvertifmagickis absent). Add crop offsets back: full_x = crop_x + offset_x, full_y = crop_y + offset_y. Author fields.json withimage_width/image_heightpixel coordinates. check_bounding_boxes.py fields.json— fix ALL intersecting boxes and too-small-for-font boxes before filling.fill_pdf_form_with_annotations.py— auto-detects PDF-vs-image coordinate system.- Verify:
convert_pdf_to_images.pyon the filled output; check placement visually. Mispositioned → Approach A: PDF-point origin check; Approach B: image-dimension/pixel match.
Verification
- Forms:
check_bounding_boxes.pypasses pre-fill; filled PDF re-imaged and visually checked - Extraction:
pdftotextround-trip returns expected text - Creation: output opens clean; page count/size as specified
Removed 2026-09: per-task Python/CLI recipe dumps, form-JSON schema examples, option catalogs, duplicated library-selection prose, LibreOffice install walkthrough, and common-issue restatements — moved to reference.md or deleted.