Skip to content

pdf://

Producer-only PDF component with seven operations. The first four are the core operations; split / rotate / fillForm cover common document manipulation:

  • op=create (default) — render HTML / structured body to PDF bytes via the configured mode.
  • op=extractText — read PDF bytes from the inbound body and output plain text. Powered by github.com/ledongthuc/pdf.
  • op=merge — concatenate multiple PDFs into one. Body shape [][]byte (preferred) or header pdf.FilesToMerge: []string of file paths. Powered by pdfcpu.
  • op=append — render header pdf.AppendData with this endpoint's mode and append the result after the inbound PDF body.
  • op=split — split the inbound PDF into chunks of pdf.Span pages (default 1 = one page each). Output is [][]byte, symmetric with op=merge's input. Powered by pdfcpu.
  • op=rotate — rotate pages clockwise by pdf.Rotation degrees (a non-zero multiple of 90). Optional pdf.Pages selection limits which pages turn. Powered by pdfcpu.
  • op=fillForm — fill interactive form fields from pdf.FormData (field name → value). Text and choice fields are set directly; checkboxes and radio buttons are toggled to the requested state. Powered by the pdfium form engine (via klippa-app/go-pdfium, WebAssembly — no CGO) for robustness across AcroForm / XFA / messy real-world files.

For op=create / op=append the rendering backend is selected by mode:

  • mode=chrome (default) — uses chromedp to drive headless Chromium / Chrome and PrintToPDF the exchange body (which must be an HTML string or []byte). Maximum fidelity: Tailwind, custom fonts, complex CSS, embedded charts. Requires a chromium / google-chrome binary on PATH.
  • mode=native — uses go-pdf/fpdf (pure-Go, no external deps) to build a PDF from a structured document body. Useful when Chromium isn't available or when you want byte-identical reproducible output.

Producer-only — CreateConsumer returns ErrConsumerNotSupported.

URI shape

pdf://<name>?op=create&mode=chrome&paperWidth=8.5&paperHeight=11&margin=0.5in&printBackground=true
pdf://<name>?op=create&mode=native&font=Helvetica&fontSize=11&title=Report
pdf://<name>?op=extractText
pdf://<name>?op=merge
pdf://<name>?op=append&mode=native
pdf://<name>?op=split
pdf://<name>?op=rotate
pdf://<name>?op=fillForm

split / rotate / fillForm take no URI params of their own — they are driven entirely by the inbound body plus the pdf.* headers below.

Omitting op= defaults to create (preserves pre-existing route behavior).

Parameters

General

Param Type Default Description
op enum create Producer operation: create / extractText / merge / append / split / rotate / fillForm.
mode enum chrome Rendering backend for op=create and op=append: chrome or native.
paperWidth number 8.5 Paper width (inches).
paperHeight number 11 Paper height.
margin string 0.5in Uniform margin (0.5in, 12mm, 1cm, 36pt). Overridden by marginTop/Bottom/Left/Right.
marginTop, marginBottom, marginLeft, marginRight number 0.5 Per-side margin (inches).
landscape boolean false Landscape orientation.

chrome mode

Param Type Default Description
printBackground boolean true Print backgrounds / images.
scale number 1.0 Scale factor (0.1–2.0).
preferCSSPageSize boolean false Honor CSS @page size.
displayHeaderFooter boolean false Show header/footer.
headerTemplate, footerTemplate string — HTML templates for header / footer.
chromePath string autodiscover Explicit Chromium / Chrome binary path.
timeout duration 30s Render timeout.
waitSelector string — Wait for a CSS selector to appear before printing.
waitMs integer — Fixed wait in milliseconds before printing.

native mode

Param Type Default Description
font string Helvetica Default font family (Helvetica, Times, Courier, Arial).
fontSize number 11 Default font size in points.
title string — Document title metadata.
author string — Document author metadata.
subject string — Document subject metadata.

Body contracts

chrome mode

The body must be HTML — string or []byte. The producer loads it as a data:text/html;charset=utf-8,... URL and PrintToPDFs it.

native mode

Accepts any of:

  • string or []byte — rendered as a single text block.
  • *NativeDoc / NativeDoc — Go struct.
  • map[string]any with the shape:
title: "Equity Report"
sections:
  - heading: "Overview"
    paragraphs: ["Q3 was strong.", "Q4 outlook positive."]
tables:
  - headers: ["Ticker", "Return"]
    rows:
      - ["AAPL", "+5.2%"]
      - ["GOOG", "+3.4%"]
text: "Trailing paragraph rendered after sections."

Headers

Read (input)

Header Used by Description
pdf.AppendData op=append The content (HTML for mode=chrome, structured map for mode=native) to render as the appended page. Required.
pdf.FilesToMerge op=merge []string of file paths to merge — used when the inbound body is not already a [][]byte.
pdf.MaxPages op=extractText Optional cap on the number of pages parsed. Default: all pages.
pdf.Span op=split Pages per output chunk. Default 1 (one page per PDF); 0 splits on bookmarks. Accepts an int or numeric string.
pdf.Rotation op=rotate Clockwise degrees — a non-zero multiple of 90 (90, 180, 270, -90). Required for rotate. Accepts an int or numeric string.
pdf.Pages op=rotate Optional page selection: a comma/space list or range string ("1-3,5"), or a list ([1,2,5]). Empty / unset = all pages.
pdf.FormData op=fillForm Field name → value. A map[string]string / map[string]any, a JSON string ('{"field":"value"}'), or JSON bytes. In YAML set-header, pass it as a JSON string constant: or a Starlang dict — a nested YAML map under constant: is a type error. Required for fillForm.

Written (output)

Header Description
Content-Type application/pdf for create/merge/append/rotate/fillForm; text/plain; charset=utf-8 for extractText.
pdf.op Operation that ran (create / extractText / merge / append / split / rotate / fillForm).
pdf.mode The backend that rendered (chrome or native). Set on create and append.
pdf.bytes Output size in bytes. Set on every op that returns a single PDF.
pdf.pages (extractText) Number of pages parsed. Page text is joined with form-feed \f separators.
pdf.sources (merge) Number of inputs merged.
pdf.parts (split) Number of output chunks in the [][]byte body.
pdf.rotation (rotate) The clockwise degrees applied.
pdf.fieldsSet (fillForm) Number of form fields actually set. 0 is reported as an error listing the available field names.

The body is set to the rendered []byte for op ∈ {create, merge, append, rotate, fillForm}, to a [][]byte for op=split, or to the extracted string for op=extractText. The exchange's In() body is also overwritten so the next pipeline step sees the result.

Examples

Render a Tailwind report via headless Chrome:

workflows:
  - id: report
    trigger: { uri: direct://render }
    steps:
      - template:
          path: templates/report.html
          engine: html
      - target: pdf://report?mode=chrome&paperWidth=8.5&paperHeight=11&margin=0.5in&printBackground=true
      - target: s3://reports/2026-q1.pdf

Native mode from a structured payload:

- set-body:
    starlang: |
      %sl 2.0
      output application/json
      input payload
      ---
      {
        "title": "Daily Briefing",
        "sections": [
          { "heading": "Markets", "paragraphs": payload.markets },
          { "heading": "News",    "paragraphs": payload.headlines }
        ]
      }
- target: pdf://briefing?mode=native&title=Daily%20Briefing&font=Helvetica&fontSize=11

Wait for a chart to render before printing:

- target: pdf://chart-page?mode=chrome&waitSelector=.chart-ready&printBackground=true

Extract text from an uploaded PDF for downstream RAG / search:

- from: http-server:///upload
  steps:
    - target: pdf://uploaded?op=extractText
    - target: rag:ingest:notes

Merge two PDFs into one:

- aggregate:
    correlationKey: 'header.batch'
    completionTimeout: 5s
    body:
      starlang: |
        %sl 2.0
        output application/json
        input payload
        ---
        payload          # list of PDF []byte
- target: pdf://join?op=merge

Append a summary page to an existing report:

- set-header:
    name: pdf.AppendData
    starlang: |
      %sl 2.0
      output application/json
      input payload
      ---
      {"title": "Summary", "text": payload.summary}
- target: pdf://report?op=append&mode=native

Split a large PDF into one file per page (fan out for per-page processing) — pdf.Span=1 is the default:

- from: file:///inbox?include=.*\.pdf
  steps:
    - set-header: { name: pdf.Span, constant: 1 }
    - target: pdf://incoming?op=split       # body → [][]byte, one PDF per page
    - split:                                 # fan out the [][]byte
        body: { starlang: payload }
    - target: pdf://page?op=extractText
    - target: rag:ingest:pages

Rotate scanned pages 90° clockwise, only pages 2 and 4:

- set-header: { name: pdf.Rotation, constant: 90 }
- set-header: { name: pdf.Pages, constant: "2,4" }
- target: pdf://scan?op=rotate

Fill an AcroForm from JSON. pdf.FormData must be a JSON string constant (a nested YAML map under constant: is a type error) — or a Starlang dict, shown second:

# (a) static JSON string constant
- set-header:
    name: pdf.FormData
    constant: '{"firstName": "Ada", "lastName": "Lovelace", "subscribe": "yes"}'
- target: pdf://application?op=fillForm
# (b) built dynamically from the payload with Starlang (→ map[string]any)
- set-header:
    name: pdf.FormData
    starlang: |
      %sl 2.0
      output application/json
      input payload
      ---
      {"firstName": payload.first, "lastName": payload.last, "subscribe": "on"}
- target: pdf://application?op=fillForm

Behavior notes

  • Chromium discovery — chromedp picks the binary off PATH. Set chromePath to point at a custom location (e.g. inside a container without chromium-browser aliased).
  • Sandboxing — chromedp.NoSandbox is set; the wrapper is intended to run in trusted environments (typically inside a server container where you control the input HTML).
  • Data URL encoding — escapeForDataURL percent-encodes % and #; the rest of the HTML is passed verbatim. This avoids the URL-fragment trap (#) while keeping the data URL small.
  • native mode page size — defaults to US Letter; non-default paperWidth/paperHeight invokes fpdf.NewCustom with explicit dimensions in inches.
  • Both modes overwrite the In() body — the next pipeline step sees the PDF bytes, not the HTML / structured input.
  • op=split output is [][]byte — pair it with a split: step to fan each chunk out as its own exchange, or a merge later to reassemble. pdf.Span=0 splits on top-level bookmarks instead of a fixed page count.
  • op=rotate needs a multiple of 90 — pdf.Rotation of 0 or a non-multiple errors rather than silently no-op'ing. 90/270/-90 swap page width/height; 180 keeps them. pdf.Pages is optional (all pages when unset) and accepts ranges ("1-3,5") or a list.
  • op=fillForm field-type coverage — text and choice (dropdown/list) fields are set by value; checkboxes and radio buttons are toggled to match the value's truthiness (yes/on/true/1 = checked; empty/no/false/0 = unchecked). Push buttons and signature fields aren't fillable and are skipped. pdf.fieldsSet reports how many fields actually changed; if none of the requested names match, the op errors with the list of available field names. Setting a checkbox writes a valid appearance state (not a raw string), so the output stays readable by strict PDF tooling.
  • pdfium form engine — op=fillForm runs the same embedded pdfium WebAssembly module that vlm:// uses to rasterise PDFs (no CGO, no external binary). It opens AcroForm, hybrid, and damaged files that a pure-Go structural editor would reject.

Source

  • pkg/component/pdf/pdf.go — component, endpoint, op dispatch, op=create chrome/native rendering.
  • pkg/component/pdf/ops.go — op=extractText / merge / append / split / rotate.
  • pkg/component/pdf/formfill.go — op=fillForm (pdfium form engine).

See also

  • chart — chart rendering (often used together — render a chart, embed into HTML, PDF the result).
  • template — template rendering for the HTML you feed into chrome mode.
  • s3, file — common downstream sinks for the rendered PDF.