> ## Documentation Index
> Fetch the complete documentation index at: https://docs.profy.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# PDF Documents

> profy-pdf — two orthogonal paths: WeasyPrint for creation (mandatory) and pypdf/qpdf for existing files, plus the kami typesetting engine

# PDF Documents (profy-pdf)

PDF work splits into two unrelated paths. **Establish which one you are on first** — everything else follows from that:

| What you want                                     | Path         | Tooling                                      |
| ------------------------------------------------- | ------------ | -------------------------------------------- |
| Produce a new PDF                                 | Creation     | **WeasyPrint** (HTML + CSS → PDF), mandatory |
| Read / merge / split / fill / OCR an existing PDF | Manipulation | pypdf, pdfplumber, qpdf, pdftotext           |

## Activation

`profy-pdf` is `user_selectable: false` — **automatically available, nothing to tick**.

```json theme={null}
{ "id": "profy-pdf", "version": "1.1.0", "activation": { "user_selectable": false } }
```

`contracts` declares only `skills: ["skills/"]` and **no tools**. This plugin ships **two** skills: `builtin/pdf` for general processing and `builtin/kami` for typesetting, covered in its own section below.

## Creation path: WeasyPrint is the only option

<Warning>
  **Never use reportlab, fpdf2, or any coordinate-based drawing library for PDF creation.** Those libraries require manually positioning every element, which produces poor output and is effectively unmaintainable — change one sentence and every subsequent position has to be recomputed by hand.
</Warning>

WeasyPrint's approach is to write HTML + CSS and render it, so pagination and layout are handled by a CSS layout engine. You get **declarative typesetting** rather than manual placement.

```python theme={null}
from weasyprint import HTML

html_content = """
<!DOCTYPE html>
<html>
<head>
<style>
  @page { size: A4; margin: 2cm; }
  body { font-family: 'Noto Sans CJK SC', sans-serif; font-size: 12pt; line-height: 1.6; }
  h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px; }
  table { width: 100%; border-collapse: collapse; margin: 1em 0; }
  th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
  th { background: #3498db; color: white; }
  tr:nth-child(even) { background: #f8f9fa; }
</style>
</head>
<body>
  <h1>Quarterly Sales Report</h1>
  <p>This report covers Q4 2024 performance.</p>
  <table>
    <tr><th>Product</th><th>Units</th><th>Revenue</th></tr>
    <tr><td>Product A</td><td>1,200</td><td>$360,000</td></tr>
  </table>
</body>
</html>
"""

HTML(string=html_content).write_pdf("report.pdf")
```

### Headers, footers, and pagination

`@page` rules are a print-specific CSS capability — unusable in a browser, essential here:

```python theme={null}
html = """
<style>
  @page {
    size: A4;
    margin: 2.5cm 2cm;
    @top-center { content: "Confidential"; font-size: 9pt; color: #999; }
    @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 9pt; }
  }
  .page-break { page-break-after: always; }
</style>
<h1>Cover</h1>
<div class="page-break"></div>
<h2>Chapter One</h2>
"""
HTML(string=html).write_pdf("multi-page.pdf")
```

`counter(page)` and `counter(pages)` are filled in by the renderer after pagination — precisely the thing a coordinate-based approach cannot give you, since it does not know the page count until it has already placed everything.

### Design guidance

* Use CSS Grid / Flexbox for complex layouts such as cards and multi-column blocks
* Drive page size, margins, headers, and footers entirely from `@page`
* Control pagination with `page-break-before` / `page-break-after`
* **For CJK text use `font-family: 'Noto Sans CJK SC', sans-serif`** (preinstalled in the sandbox)
* For branded reports, set accent colours and use CSS background shapes

### When not to use WeasyPrint

| Situation                          | Use instead                       |
| ---------------------------------- | --------------------------------- |
| Filling an existing PDF form       | pypdf (see FORMS.md in the skill) |
| Merging or splitting existing PDFs | pypdf, qpdf                       |
| Extracting text or tables          | pdfplumber                        |

## Manipulation path: command-line tools

### pdftotext (poppler-utils)

```bash theme={null}
pdftotext input.pdf output.txt                 # extract text
pdftotext -layout input.pdf output.txt         # preserve layout
pdftotext -f 1 -l 5 input.pdf output.txt       # pages 1-5 only
```

`-layout` matters more than it looks: without it, multi-column layouts come out as interleaved nonsense, because the extractor follows the text stream rather than the visual columns.

### qpdf

```bash theme={null}
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf   # merge
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf             # split
qpdf input.pdf output.pdf --rotate=+90:1                 # rotate page 1 by 90°
qpdf --password=mypassword --decrypt in.pdf out.pdf      # remove password
```

### pdftk (if available)

```bash theme={null}
pdftk file1.pdf file2.pdf cat output merged.pdf
pdftk input.pdf burst
pdftk input.pdf rotate 1east output rotated.pdf
```

### Form handling

The skill ships a set of form scripts: `extract_form_field_info.py`, `extract_form_structure.py`, `check_fillable_fields.py`, `fill_fillable_fields.py`, `fill_pdf_form_with_annotations.py`, plus verification helpers `check_bounding_boxes.py`, `create_validation_image.py`, and `convert_pdf_to_images.py`.

The workflow is **probe before filling**. Form field naming varies arbitrarily between PDFs, so guessing field names is guaranteed to fail — and to fail silently, since writing to a non-existent field is not an error. Extract the real structure first.

### OCR for scanned documents

Scanned PDFs have no text layer, so `pdftotext` returns nothing. These require `pytesseract` plus `pdf2image` to OCR into searchable text.

## kami · 紙: the typesetting engine

`builtin/kami` is the second skill inside the pdf plugin, dedicated to **deliverables that need to look designed**: warm parchment canvas, ink-blue accent, serif-led hierarchy, tight editorial rhythm.

### Nine document types

| User says                                                  | Document      |
| ---------------------------------------------------------- | ------------- |
| one-pager / exec summary                                   | One-Pager     |
| white paper / long-form / annual review / technical report | Long Doc      |
| formal letter / resignation / recommendation / memo        | Letter        |
| portfolio / case studies                                   | Portfolio     |
| resume / CV                                                | Resume        |
| slides / deck / presentation                               | Slides        |
| equity report / valuation analysis / investment memo       | Equity Report |
| changelog / release notes                                  | Changelog     |
| landing page / product page                                | Landing Page  |

Selection runs through a decision tree rather than a question. Ask only when two rows genuinely both fit:

| Signal                                                        | Document                                      |
| ------------------------------------------------------------- | --------------------------------------------- |
| Length target unknown                                         | **Ask "how many pages" first**, then classify |
| ≤ 1 page + investor / recruiter / exec audience               | one-pager                                     |
| ≤ 1 page + formal correspondence                              | letter                                        |
| 1.5–2 pages + career narrative + project bullets              | resume                                        |
| 3–6 pages + project showcase + visually heavy                 | portfolio                                     |
| 6–15 pages + sustained argument + low visual density          | long-doc                                      |
| Presentation flow + speaker support + one assertion per slide | slides                                        |
| Financial dashboard + thesis + price or risk view             | equity-report                                 |
| Version-by-version log                                        | changelog                                     |
| Product showcase + pricing + screenshots + FAQ                | landing-page                                  |

<Info>
  **Landing Page produces no PDF.** It is a screen-first interactive template with a gallery carousel, hero entrance animation, responsive breakpoints at 880px and 480px, and `prefers-reduced-motion` support. The deliverable is a ready-to-host `.html` file.
</Info>

Slides default to `slides-weasy.html` (WeasyPrint HTML → PDF); the PPTX-producing variant is used only when the user explicitly needs an editable PowerPoint file.

### Fonts

| Language | Primary                                                                        | Fallback chain                                                                              |
| -------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| Chinese  | TsangerJinKai02-W04 (body) + W05 (headings, true bold)                         | Source Han Serif SC → Noto Serif CJK SC → Songti SC → STSong → Georgia                      |
| English  | Charter for both headings and body (`--sans: var(--serif)`, one font per page) | Georgia → Palatino → Times New Roman                                                        |
| Japanese | Best-effort, no dedicated templates                                            | YuMincho → Hiragino Mincho ProN → Noto Serif CJK JP → Source Han Serif JP → TsangerJinKai02 |

The Chinese fonts are commercial. Before building Chinese documents, run the font recovery script once:

```bash theme={null}
bash scripts/ensure-fonts.sh
```

It tries several CDN sources with retry and size validation, and suggests Source Han Serif SC as a fallback if all of them fail.

<Warning>
  Japanese currently rides the CJK template path with no dedicated `-ja` templates. Before shipping, **visually verify line breaks, punctuation rhythm, and emphasis weight** — those three are exactly where Chinese and Japanese typesetting conventions diverge.
</Warning>

### Handling vague feedback

When a user says "looks off", "too cramped", or "not elegant", the skill forbids guessing and requires asking back **with the current values**. The value of this rule is that it converts a subjective argument into a parameter adjustment: "line-height is currently 1.6, should it go to 1.8?" resolves far faster than "let me try again".

## Boundaries and failure modes

* **Creation has exactly one supported route**, WeasyPrint; coordinate-drawing libraries are explicitly prohibited.
* **Scanned documents require OCR**, or text extraction returns empty.
* **Form fields must be probed first**; guessing names fails silently.
* **kami's Chinese fonts are commercial** and are not bundled with distributions — they are fetched by `ensure-fonts.sh`.
* **Landing Page has no PDF output.** Do not expect to export one.

### Troubleshooting

| Symptom                                     | Cause                                              | Fix                                                                            |
| ------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------ |
| CJK text renders as boxes or missing glyphs | No CJK font specified, or kami fonts absent        | Set `'Noto Sans CJK SC'`; for kami run `ensure-fonts.sh` first                 |
| `pdftotext` returns nothing                 | Scanned document with no text layer                | Use the pytesseract OCR route                                                  |
| Multi-column text comes out scrambled       | `-layout` was omitted                              | Re-run with `-layout`                                                          |
| Page numbers print as literal text          | `counter(page)` unused, or written outside `@page` | Page numbers can only be generated inside an `@page` margin box                |
| Form appears empty after filling            | Field names were guessed wrong                     | Run `extract_form_field_info.py` for the real names first                      |
| Page breaks land in the wrong place         | Wrong break property, or blocked by a container    | Use `page-break-after: always` and check for an enclosing `overflow` container |

## Verify your output

1. Convert to images and review every page: `convert_pdf_to_images.py`. **Layout accidents are invisible in the source.**
2. For Chinese documents, search for box glyphs — the classic signature of a font that never loaded.
3. On multi-page documents, jump to the last page and confirm that `of N` shows a real page count rather than literal text.

## Related

<CardGroup cols={2}>
  <Card title="Office documents overview" href="/en/documentation/capabilities/office-documents">
    How the four document capabilities differ and when to use each
  </Card>

  <Card title="Presentations" href="/en/documentation/capabilities/office-pptx">
    When the deliverable is a deck rather than a document
  </Card>
</CardGroup>

<Note>
  Verified 2026-08-11. Sources: `services/agent-runtime/src/plugins/builtin/pdf/plugin.json`, `skills/pdf/SKILL.md`, `skills/kami/SKILL.md`, `skills/pdf/scripts/`, `skills/kami/scripts/ensure-fonts.sh`.
</Note>
