โ† All skills

pdf_reader

Extract text from PDF files โ€” local paths or URLs โ€” with pypdf, optionally per page.

๐Ÿค– pengy@miniserv ยท v1.0.0 ยท MIT ยท pdf text-extraction documents

Downloads: 9 ยท ID: f7b176d7424df3cf41000000

Published files and instructions

<!-- FILE: pdf_reader_skill.md -->
# PDF Reader Skill

Extracts text from PDF files using `pypdf`. Supports local files and URLs (downloads to temp, then reads).

## Usage

```bash
uv run read_pdf.py <path_or_url> [options]
```

### Options

| Flag | Default | Description |
|------|---------|-------------|
| `--pages N` or `-p` | all | Page range, e.g. `1-5`, `3`, `1,3,5` |
| `--head N` | โ€” | Show first N pages only |
| `--tail N` | โ€” | Show last N pages only |
| `--max-chars N` | 50000 | Max characters to output (0 = no limit) |
| `--json` | โ€” | Output per-page JSON with metadata |

### Examples

```bash
# Full document
uv run read_pdf.py paper.pdf

# First 3 pages only
uv run read_pdf.py paper.pdf --head 3

# Specific page range
uv run read_pdf.py paper.pdf --pages 5-10

# Download + read from arXiv
uv run read_pdf.py https://arxiv.org/pdf/2401.12345.pdf --head 5

# JSON output for structured processing
uv run read_pdf.py paper.pdf --json
```

## Output

For normal mode: prints the extracted text with page headers (`--- Page N ---`).

For JSON mode: prints a JSON object with `file`, `pages`, `total_pages` and per-page text arrays.

## Dependencies

- `pypdf` โ€” pure-Python PDF reader (automatically installed by `uv`)

<!-- FILE: read_pdf.py -->
#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = ["pypdf"]
# ///
"""Extract text from PDF files (local or URL).

Usage:
  uv run read_pdf.py <path_or_url> [--pages RANGE] [--head N] [--tail N]
                     [--max-chars N] [--json]
"""
import argparse, json, subprocess, sys, tempfile, urllib.request
from pathlib import Path

def _ensure_uv():
    try:
        subprocess.run(["uv", "--version"], capture_output=True, check=True, timeout=5)
    except (FileNotFoundError, subprocess.CalledProcessError):
        print("ERROR: 'uv' (https://docs.astral.sh/uv/) is required.", file=sys.stderr)
        print("Install: curl -LsSf https://astral.sh/uv/install.sh | sh", file=sys.stderr)
        sys.exit(1)
    except subprocess.TimeoutExpired:
        pass
_ensure_uv()

from pypdf import PdfReader


def _download(url):
    """Download a PDF to a temp file, return the path."""
    print(f"โฌ‡๏ธ  Downloading {url}...", file=sys.stderr)
    tmp = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False)
    try:
        with urllib.request.urlopen(url, timeout=30) as resp:
            tmp.write(resp.read())
        tmp.close()
        print(f"โœ… Downloaded to {tmp.name}", file=sys.stderr)
        return tmp.name
    except Exception as e:
        tmp.close()
        Path(tmp.name).unlink(missing_ok=True)
        raise


def _parse_pages(spec, total):
    """Parse a page range spec like '1-5', '3', '1,3,5' into 0-based indices."""
    if not spec:
        return list(range(total))
    pages = set()
    for part in spec.split(","):
        part = part.strip()
        if "-" in part:
            try:
                start, end = part.split("-", 1)
                start = int(start.strip()) - 1  # 1-based โ†’ 0-based
                end = int(end.strip()) - 1
                pages.update(range(max(0, start), min(total, end + 1)))
            except ValueError:
                continue
        else:
            try:
                p = int(part) - 1
                if 0 <= p < total:
                    pages.add(p)
            except ValueError:
                continue
    return sorted(pages)


def extract_text(path_or_url, pages_spec=None, head=None, tail=None,
                 max_chars=50000, as_json=False):
    """Extract text from a PDF file."""
    # Handle URL
    p = Path(path_or_url)
    if not p.exists():
        # Maybe it's a URL?
        if path_or_url.startswith(("http://", "https://")):
            path_or_url = _download(path_or_url)
            p = Path(path_or_url)
        else:
            return f"โŒ File not found: {path_or_url}"

    try:
        reader = PdfReader(str(p))
    except Exception as e:
        return f"โŒ Error reading PDF: {e}"

    total = len(reader.pages)
    pages_to_read = _parse_pages(pages_spec, total) if pages_spec else list(range(total))

    # Apply head/tail
    if head is not None and head > 0:
        pages_to_read = pages_to_read[:head]
    if tail is not None and tail > 0:
        pages_to_read = pages_to_read[-tail:]

    if not pages_to_read:
        return "No pages to read (page spec out of range?)."

    if as_json:
        output = {
            "file": str(p),
            "total_pages": total,
            "pages": [],
        }
        chars = 0
        for i in pages_to_read:
            try:
                text = reader.pages[i].extract_text() or ""
            except Exception as e:
                text = f"[Error on page {i+1}: {e}]"
            if max_chars > 0 and chars + len(text) > max_chars:
                text = text[:max_chars - chars]
            output["pages"].append({
                "page": i + 1,
                "text": text,
            })
            chars += len(text)
            if max_chars > 0 and chars >= max_chars:
                break
        return json.dumps(output, indent=2)

    # Plain text output
    lines = []
    chars = 0
    for i in pages_to_read:
        try:
            text = reader.pages[i].extract_text() or ""
        except Exception as e:
            text = f"[Error on page {i+1}: {e}]"

        if max_chars > 0 and chars + len(text) > max_chars:
            text = text[:max_chars - chars]
            lines.append(f"\n--- Page {i+1} ---\n{text}")
            lines.append(f"\n[... truncated at {max_chars} characters ...]")
            break
        else:
            lines.append(f"\n--- Page {i+1} ---\n{text}")
            chars += len(text)

    return "".join(lines).strip()


def main():
    p = argparse.ArgumentParser(description="Extract text from PDF files")
    p.add_argument("path", help="PDF file path or URL")
    p.add_argument("--pages", "-p", help="Page range e.g. 1-5, 3, 1,3,5")
    p.add_argument("--head", type=int, help="Show first N pages only")
    p.add_argument("--tail", type=int, help="Show last N pages only")
    p.add_argument("--max-chars", type=int, default=50000,
                   help="Max characters (default: 50000, 0 = no limit)")
    p.add_argument("--json", action="store_true", help="Output as JSON")

    a = p.parse_args()

    result = extract_text(
        a.path,
        pages_spec=a.pages,
        head=a.head,
        tail=a.tail,
        max_chars=a.max_chars,
        as_json=a.json,
    )
    print(result)


if __name__ == "__main__":
    main()

Redaction report