Metadata-Version: 2.4
Name: hirarapdf
Version: 0.1.0
Summary: Self-hosted PDF read / parse / create tools for AI agents — HTTP and MCP
Author: Lucas D Marshall (Kaung Myat San)
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/lucasdmarshall/Hirara
Project-URL: Repository, https://github.com/lucasdmarshall/Hirara
Project-URL: Issues, https://github.com/lucasdmarshall/Hirara/issues
Keywords: llm,agent,mcp,pdf,pdf-parser,pdf-generation,pypdf,reportlab
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Text Processing :: Markup
Classifier: Topic :: Office/Business
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: pypdf>=5.0
Requires-Dist: reportlab>=4.0
Requires-Dist: xhtml2pdf>=0.2.11
Requires-Dist: httpx>=0.28
Requires-Dist: hirara-core>=0.1
Provides-Extra: service
Requires-Dist: fastapi>=0.115; extra == "service"
Requires-Dist: uvicorn[standard]>=0.30; extra == "service"
Requires-Dist: python-multipart>=0.0.9; extra == "service"
Provides-Extra: mcp
Requires-Dist: mcp>=2.0; extra == "mcp"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Dynamic: license-file

<div align="center">

# Hirarapdf

**Self-hosted PDF tools for AI agents — read, parse, and create. No API keys.**

Part of the [Hirara](https://github.com/lucasdmarshall/Hirara) tool hub.
Pure-Python: [pypdf](https://github.com/py-pdf/pypdf) reads,
[reportlab](https://www.reportlab.com/) writes. URL fetches route through the
hub's shared SSRF guard, [`hirara-core`](../hirara-core/).

[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/MCP-compatible-8A2BE2.svg)](https://modelcontextprotocol.io/)

</div>

---

## Three tools

| Tool | Role | What it does |
|---|---|---|
| **`pdf_read`** | reader | Full text + per-page text + metadata out of a PDF |
| **`pdf_info`** | parser | Structure only — page count, metadata, outline, form fields, page sizes |
| **`pdf_create`** | maker | A real, paginating PDF from Markdown, plain text, or HTML |

`pdf_info` is the cheap "what is this file" call — use it to decide *whether* or
*how* to read a large or unfamiliar PDF before paying to extract every page.

---

## Quick start

```bash
pip install -e ../hirara-core        # shared SSRF guard (repo-local)
pip install -e ".[service,mcp]"
```

As an HTTP service:

```bash
uvicorn hirarapdf.service:app --host 127.0.0.1 --port 8200
```

**Read** a PDF's text (base64, path, or URL):

```bash
curl -X POST localhost:8200/pdf_read -H 'content-type: application/json' \
  -d '{"pdf_path":"/path/to/report.pdf"}'
```

Or upload it directly:

```bash
curl -X POST localhost:8200/pdf_read/upload -F file=@report.pdf
```

**Inspect** structure without extracting all the text:

```bash
curl -X POST localhost:8200/pdf_info -H 'content-type: application/json' \
  -d '{"pdf_path":"/path/to/report.pdf"}'
```

**Create** a PDF from Markdown — returns `pdf_base64`, or writes to `output_path`:

```bash
curl -X POST localhost:8200/pdf_create -H 'content-type: application/json' \
  -d '{"content":"# Hello\n\n- one\n- two","title":"Demo","output_path":"out.pdf"}'
```

As an MCP server over stdio (Claude Desktop / Cursor / agent runtimes):

```bash
python -m hirarapdf.mcp_server
```

With Docker (loopback-bound, nothing to download at boot):

```bash
docker compose up -d --build
```

---

## The maker's input formats

`pdf_create` accepts three `format`s, all rendered **pure-Python** on top of
reportlab — no browser engine, no cairo/pango system libraries, the usual way
"HTML to PDF" balloons a container.

**`markdown`** — a lightweight subset, deliberately *not* full CommonMark:
`#`/`##`/`###` headings, `-`/`*` bullet lists, `1.` numbered lists,
```` ``` ```` fenced code blocks, and inline `**bold**`, `*italic*`, `` `code` ``.
Not supported: tables, images, nested blockquotes. User content is XML-escaped
*before* the inline markup is applied, so a document that literally contains
`<script>` or `&` renders as text, not broken markup.

**`text`** — plain text; blank lines separate paragraphs. No markup at all.

**`html`** — a practical subset of HTML + CSS, rendered by
[xhtml2pdf](https://github.com/xhtml2pdf/xhtml2pdf): headings, paragraphs,
lists, **tables**, images, and simple inline/`@page` CSS. It is not a modern
layout engine — JavaScript, flexbox and grid do not apply, so feed it
document-style HTML. A bare fragment is wrapped in a minimal document (so
`title`/`author`/`subject` reach the PDF metadata); a full `<html>` document is
passed through and controls its own `<head>`.

```bash
curl -X POST localhost:8200/pdf_create -H 'content-type: application/json' \
  -d '{"format":"html","content":"<h1>Invoice</h1><table><tr><td>A</td><td>1</td></tr></table>","title":"Invoice"}'
```

---

## Security

This tool turns attacker-influenceable bytes into and out of PDFs, so three
levers matter:

**`pdf_url` is off by default.** When you enable it
(`CPDF_ALLOW_URL_FETCH=true`), the fetch goes through `hirara-core`'s
`safe_download` — resolve-then-pin, every redirect hop re-validated, streamed
under a hard byte cap — so a public URL that `302`s to `169.254.169.254` is
rejected at the hop. It is never a bare `httpx.get`.

**Local filesystem access is gated.** `pdf_path` (read) and `output_path`
(write) let a caller name any path on the host. Fine for a local MCP server or a
loopback service you trust; **off in the Docker image** because a
network-exposed service should not read or write arbitrary files. Set
`CPDF_ALLOW_LOCAL_PATH=true` only for a trusted, local deployment.

**Everything is size-capped.** Input PDFs are bounded by `CPDF_MAX_BYTES`,
extracted text by `CPDF_MAX_CHARS` (the result flags `truncated=true`), and
`pdf_create` input by `CPDF_MAX_CREATE_CHARS`.

Errors come back in the response body, not as HTTP status codes — the caller is
an agent loop, and "PDF too large" is a result to reason about, not a transport
failure. Every response carries the same keys whether it succeeded or failed.

> **Note — no OCR.** `pdf_read` extracts a PDF's embedded text layer. A scanned
> PDF that is just images with no text layer comes back with little or no text.
> OCR is a separate concern (and a candidate for a future hub tool).

---

## API

| Endpoint | Purpose |
|---|---|
| `POST /pdf_read` | Extract text. `pdf_path` / `pdf_url` / `pdf_base64`, `password`, `include_pages` |
| `POST /pdf_read/upload` | Same, multipart file upload |
| `POST /pdf_info` | Structure only. Same sources |
| `POST /pdf_info/upload` | Same, multipart file upload |
| `POST /pdf_create` | Build a PDF. `content`, `format`, `title`, `author`, `subject`, `page_size`, `output_path` |
| `GET /schemas` | Tool definitions, ready for an LLM `tools` array |
| `GET /health` | Liveness + effective config |

---

## Configuration

All via environment variable.

| Variable | Default | Notes |
|---|---|---|
| `CPDF_MAX_BYTES` | `26214400` | Cap on an input PDF (25 MB) |
| `CPDF_MAX_CHARS` | `200000` | Cap on extracted text; result flags `truncated` |
| `CPDF_ALLOW_URL_FETCH` | `false` | Enable `pdf_url` (through the SSRF guard) |
| `CPDF_URL_TIMEOUT` | `30` | Seconds for a URL fetch |
| `CPDF_ALLOW_LOCAL_PATH` | `true` (lib) / `false` (image) | `pdf_path` read + `output_path` write |
| `CPDF_PAGE_SIZE` | `A4` | Default page size for `pdf_create` |
| `CPDF_MAX_CREATE_CHARS` | `500000` | Cap on `pdf_create` input length |

---

## Testing

```bash
pip install -e ".[dev]"
pytest -q
```

The suite roundtrips through both engines — the maker builds a PDF, the reader
reads it back — so no binary fixtures are checked in. It also proves `pdf_url`
routes through the shared SSRF guard and surfaces a block as an error envelope
rather than an exception.

---

## License

Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).

Copyright 2026 Lucas D Marshall (Kaung Myat San).
