Claude
Skills
Sign in
Back

pdf-builder

Included with Lifetime
$97 forever

Generate branded PSD PDF documents with letterhead, clean fonts, and Documenso-ready field coordinates. Use when: creating forms, permission slips, agreements, contracts, waivers, board resolutions, or any document needing PSD branding and/or digital signing. Triggers on: create pdf, build pdf, branded pdf, generate form, pdf builder, letterhead document, signable document.

Writing & Docsscripts

What this skill does


# PDF Builder — PSD Branded Document Generator

Generate professional PDFs with Peninsula School District letterhead, Inter/Josefin Sans fonts, and Documenso-ready field manifests for digital signing workflows.

## Quick Start

### Using a Built-in Template

```bash
uv run generate_pdf.py --template <template-name> --data '<json>' --output ~/Downloads/doc.pdf
```

### Using a Custom JSON Spec

```bash
uv run generate_pdf.py --json '<spec>' --output ~/Downloads/doc.pdf
# or
uv run generate_pdf.py --spec spec.json --output ~/Downloads/doc.pdf
```

> **Note**: Output paths must be within the skill's `paths:` scope (`scripts/`, `references/`, `~/Downloads/`, `~/Desktop/`).

### Output

Every invocation produces:
1. **PDF file** at the `--output` path
2. **Field manifest** at `{output}.fields.json` — Documenso-ready percentage coordinates
3. **Stdout JSON**: `{"pdf": "...", "manifest": "...", "pages": N, "fields": N}`

## Scripts

All scripts live in `plugins/psd-productivity/skills/pdf-builder/scripts/`.

| Script | Purpose |
|--------|---------|
| `generate_pdf.py` | Core PDF generator — takes JSON spec or template name |
| `letterhead.py` | Letterhead module — logo, colors, footer, continuation pages |
| `install_fonts.py` | One-time font setup — downloads Inter + Josefin Sans |

**Runtime**: All scripts use PEP 723 inline dependencies. Run with `uv run`.

### First-Time Setup

Before first use, install fonts:

```bash
cd plugins/psd-productivity/skills/pdf-builder/scripts
uv run install_fonts.py
```

Fonts are stored in `scripts/fonts/` (checked into repo). This only needs to run once.

## Built-in Templates

| Template | Description | Signers |
|----------|-------------|---------|
| `permission-slip` | Student activity permission | 1 (parent) |
| `employment-agreement` | HR hiring document | 2 (employee + HR) |
| `contractor-agreement` | Vendor/contractor MOU | 2 (contractor + district) |
| `policy-acknowledgment` | Staff policy receipt | 1 (employee) |
| `field-trip-waiver` | Liability waiver + emergency info | 1 (parent) |
| `board-resolution` | Board action item | 2 (chair + secretary) |
| `leave-request` | Staff leave form | 3 (employee + supervisor + HR) |
| `generic-form` | Blank letterhead + title | Configurable |

### Template Data Variables

Pass data to templates via `--data` JSON. Each template accepts different variables.

**permission-slip**: `student_name`, `event`, `date`, `school`, `grade`, `title`, `body`
**employment-agreement**: `employee_name`, `position`, `department`, `location`, `terms`
**contractor-agreement**: `terms`
**policy-acknowledgment**: `title`, `body`
**board-resolution**: `resolution_number`, `subject`, `body`, `resolution_text`
**field-trip-waiver**: `student_name`, `destination`, `title`, `body`
**leave-request**: *(no data variables — all fields are fillable)*
**generic-form**: `title`, `body`

## Custom Document Spec

For documents that don't fit a template, build a JSON spec with sections:

```json
{
  "title": "Document Title",
  "department": "Technology Department",
  "data": {
    "name": "Jane Doe"
  },
  "sections": [
    {"type": "heading", "text": "Section Title", "level": 1},
    {"type": "paragraph", "text": "Hello {{name}}, this is body text."},
    {"type": "field_row", "fields": [
      {"label": "Full Name", "type": "TEXT", "width": 0.5},
      {"label": "Date", "type": "DATE", "width": 0.5}
    ]},
    {"type": "checkbox_group", "label": "Options", "items": [
      {"label": "Option A"},
      {"label": "Option B", "checked": true}
    ]},
    {"type": "table", "headers": ["Col1", "Col2"], "rows": [["A", "B"]]},
    {"type": "divider"},
    {"type": "spacer", "height": 20},
    {"type": "signature_block", "signers": [
      {"role": "Employee", "fields": ["SIGNATURE", "DATE"]},
      {"role": "Supervisor", "fields": ["SIGNATURE", "DATE", "NAME"]}
    ]}
  ]
}
```

### Section Types

| Type | Description | Key Properties |
|------|-------------|----------------|
| `heading` | Josefin Sans Bold title | `text`, `level` (1=18pt, 2=14pt, 3=11pt) |
| `paragraph` | Inter Regular body text (auto-wrapped) | `text`, `fontSize`, `lineHeight` |
| `field_row` | Horizontal row of labeled input boxes | `fields[]` with `label`, `type`, `width` (0-1 fraction), `value`, `height` (points, default 22). Section-level options: `showLabels` (bool, default `true`), `gap` (int, default ~9pt — horizontal gap between cells), `rowGap` (int, default ~9pt — vertical gap after the row) |
| `checkbox_group` | Vertical checkbox list | `label`, `items[]` with `label`, `checked`, `required` |
| `table` | Data table with alternating rows | `headers[]`, `rows[][]` |
| `signature_block` | Signing zone with role labels | `signers[]` with `role`, `fields[]`, `anchor` ("flow" or "bottom") |
| `spacer` | Vertical whitespace | `height` (points) |
| `divider` | Horizontal line | `color`, `weight` |

**CRITICAL — Duplicate field labels**: Field labels are slugified to create manifest field names. Two fields with the same label (e.g., both "Position") produce the same slug, causing the manifest `positions` object to only keep the last one. Use unique labels (e.g., "Team Member Position", "Evaluator Position") or deduplicate manually in downstream code.

**Spacer for page breaks**: A spacer of ~80pt after a signature block reliably pushes the next section to a new page. Use this to separate reference appendices from the evaluation content.

### Building a tight table of form fields

Use `showLabels: false`, `gap: 0`, `rowGap: 0` on stacked `field_row` sections to render a proper table of fillable cells (one header row with labels, many flush data rows below). Each cell still needs a unique label for slug uniqueness — compact names like `S2 First` / `S2 Last` work well.

```json
{ "type": "field_row", "gap": 0, "rowGap": 0, "fields": [
    { "label": "First Name", "type": "TEXT", "width": 0.18, "height": 14 },
    { "label": "Last Name",  "type": "TEXT", "width": 0.18, "height": 14 },
    { "label": "Student ID", "type": "TEXT", "width": 0.13, "height": 14 },
    { "label": "DOB",        "type": "TEXT", "width": 0.15, "height": 14 },
    { "label": "Grade",      "type": "TEXT", "width": 0.10, "height": 14 },
    { "label": "School",     "type": "TEXT", "width": 0.26, "height": 14 }
]},
{ "type": "field_row", "showLabels": false, "gap": 0, "rowGap": 0, "fields": [
    { "label": "S2 First", "type": "TEXT", "width": 0.18, "height": 14 },
    { "label": "S2 Last",  "type": "TEXT", "width": 0.18, "height": 14 },
    { "label": "S2 ID",    "type": "TEXT", "width": 0.13, "height": 14 },
    { "label": "S2 DOB",   "type": "TEXT", "width": 0.15, "height": 14 },
    { "label": "S2 Grade", "type": "TEXT", "width": 0.10, "height": 14 },
    { "label": "S2 School","type": "TEXT", "width": 0.26, "height": 14 }
]},
...
```

Reference implementation: `workflows/ssd-mv-intake/template-spec.json` in the `psd-workflow-automation` repo (6-student McKinney-Vento intake table).

### Page 1 title rendering

The letterhead module only renders the spec's top-level `title` in the **continuation header** on pages 2+. Page 1 has the logo block and letterhead but no title bar. To show the document title on page 1, add an explicit `heading` section at the top of `sections`:

```json
{ "type": "spacer", "height": 4 },
{ "type": "heading", "text": "Annual McKinney-Vento Intake Form", "level": 1 },
{ "type": "spacer", "height": 6 },
```

### Field Types for `field_row` and `signature_block`

| Type | Documenso Mapping | Use |
|------|------------------|-----|
| `TEXT` | TEXT/text | General text input |
| `DATE` | DATE/date | Date fields |
| `NAME` | NAME/name | Full name |
| `EMAIL` | EMAIL/email | Email address |
| `NUMBER` | TEXT/number | Numeric input |
| `SIGNATURE` | SIGNATURE/signature | Signature line |
| `INITIALS` | INITIALS/initials | Initials box |
| `CHECKBOX` | CHECKBOX/checkbox | Checkboxes |
| `DROPDOWN` | DROPDOWN/dropdown | Dropdown (provide `options`) |

Related in Writing & Docs