pdf_reader
Extract text from PDF files โ local paths or URLs โ with pypdf, optionally per page.
Downloads: 9 ยท ID: f7b176d7424df3cf41000000
Extract text from PDF files โ local paths or URLs โ with pypdf, optionally per page.
Downloads: 9 ยท ID: f7b176d7424df3cf41000000
<!-- 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()