scripts/pdf_read.py
scripts/pdf_read.pyBrowse 21 files
1,415 tokens
5,895 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Read a PDF: per-page text, tables, metadata, or form fields. JSON to stdout."""3from __future__ import annotations4 5import argparse6import csv7import json8import os9import sys10 11 12def _reconfigure_stdio() -> None:13 for stream in (sys.stdout, sys.stderr):14 try:15 stream.reconfigure(encoding="utf-8")16 except Exception:17 pass18 19 20def _need(module: str, package: str):21 try:22 return __import__(module)23 except ImportError:24 print(f"Missing dependency: install with 'python3 -m pip install {package}'", file=sys.stderr)25 raise SystemExit(2)26 27 28def read_text(path: str, password: str | None) -> dict:29 pdfplumber = _need("pdfplumber", "pdfplumber")30 pages = []31 with pdfplumber.open(path, password=password) as pdf:32 for page in pdf.pages:33 pages.append(page.extract_text() or "")34 return {"page_count": len(pages), "pages": pages}35 36 37def read_tables(path: str, password: str | None, csv_dir: str | None) -> dict:38 pdfplumber = _need("pdfplumber", "pdfplumber")39 result = []40 written = []41 with pdfplumber.open(path, password=password) as pdf:42 for pageno, page in enumerate(pdf.pages, start=1):43 for tidx, table in enumerate(page.extract_tables()):44 result.append({"page": pageno, "index": tidx, "rows": table})45 if csv_dir:46 os.makedirs(csv_dir, exist_ok=True)47 csv_path = os.path.join(csv_dir, f"page{pageno}_table{tidx}.csv")48 with open(csv_path, "w", encoding="utf-8", newline="") as fh:49 csv.writer(fh).writerows([[c if c is not None else "" for c in row] for row in table])50 written.append(csv_path)51 out = {"table_count": len(result), "tables": result}52 if csv_dir:53 out["csv_files"] = written54 return out55 56 57def read_meta(path: str, password: str | None) -> dict:58 pypdf = _need("pypdf", "pypdf")59 reader = pypdf.PdfReader(path)60 encrypted = reader.is_encrypted61 if encrypted:62 if password is None or not reader.decrypt(password):63 return {"encrypted": True, "note": "Provide --password to read metadata of an encrypted file."}64 meta = {}65 if reader.metadata:66 for key, value in reader.metadata.items():67 meta[str(key).lstrip("/")] = str(value)68 pages = []69 for idx, page in enumerate(reader.pages, start=1):70 box = page.mediabox71 pages.append({72 "page": idx,73 "width": float(box.width),74 "height": float(box.height),75 "rotation": int(page.get("/Rotate", 0)),76 })77 # scanned-page heuristic: no extractable text but page has images78 likely_scanned = []79 try:80 pdfplumber = _need("pdfplumber", "pdfplumber")81 with pdfplumber.open(path, password=password) as pdf:82 for pageno, page in enumerate(pdf.pages, start=1):83 text = (page.extract_text() or "").strip()84 if not text and page.images:85 likely_scanned.append(pageno)86 except SystemExit:87 raise88 except Exception as exc: # pragma: no cover - heuristic only89 print(f"Warning: scanned-page check failed: {exc}", file=sys.stderr)90 out = {91 "encrypted": encrypted,92 "page_count": len(reader.pages),93 "metadata": meta,94 "pages": pages,95 "likely_scanned_pages": likely_scanned,96 }97 if likely_scanned:98 out["note"] = ("Image-only pages detected: no text layer to extract. "99 "Use the references/ocr-extraction.md in this skill for OCR.")100 return out101 102 103FIELD_TYPES = {"/Tx": "text", "/Btn": "button", "/Ch": "choice", "/Sig": "signature"}104 105 106def read_fields(path: str, password: str | None) -> dict:107 pypdf = _need("pypdf", "pypdf")108 reader = pypdf.PdfReader(path)109 if reader.is_encrypted:110 if password is None or not reader.decrypt(password):111 print("File is encrypted; pass --password.", file=sys.stderr)112 raise SystemExit(3)113 fields = reader.get_fields() or {}114 out = {}115 for name, field in fields.items():116 ftype = FIELD_TYPES.get(str(field.get("/FT")), str(field.get("/FT")))117 value = field.get("/V")118 states = field.get("/_States_")119 entry = {"type": ftype, "value": None if value is None else str(value)}120 if states:121 entry["options"] = [str(s) for s in states]122 out[name] = entry123 return {"field_count": len(out), "fields": out}124 125 126def main() -> int:127 _reconfigure_stdio()128 parser = argparse.ArgumentParser(description="Extract text, tables, metadata, or form fields from a PDF.")129 parser.add_argument("pdf", help="Input PDF path")130 mode = parser.add_mutually_exclusive_group(required=True)131 mode.add_argument("--text", action="store_true", help="Per-page text as JSON")132 mode.add_argument("--tables", action="store_true", help="Tables as JSON (optionally CSV via --csv-dir)")133 mode.add_argument("--meta", action="store_true", help="Metadata, page sizes, encrypted/scanned flags")134 mode.add_argument("--fields", action="store_true", help="AcroForm fields with types and values")135 parser.add_argument("--csv-dir", help="Also write each table as a CSV file into this directory")136 parser.add_argument("--password", help="Password for encrypted PDFs")137 args = parser.parse_args()138 139 if args.text:140 result = read_text(args.pdf, args.password)141 elif args.tables:142 result = read_tables(args.pdf, args.password, args.csv_dir)143 elif args.meta:144 result = read_meta(args.pdf, args.password)145 else:146 result = read_fields(args.pdf, args.password)147 json.dump(result, sys.stdout, ensure_ascii=False, indent=2)148 print()149 return 0150 151 152if __name__ == "__main__":153 sys.exit(main())154