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 configuredmode.op=extractText— read PDF bytes from the inbound body and output plain text. Powered bygithub.com/ledongthuc/pdf.op=merge— concatenate multiple PDFs into one. Body shape[][]byte(preferred) or headerpdf.FilesToMerge: []stringof file paths. Powered bypdfcpu.op=append— render headerpdf.AppendDatawith this endpoint'smodeand append the result after the inbound PDF body.op=split— split the inbound PDF into chunks ofpdf.Spanpages (default 1 = one page each). Output is[][]byte, symmetric withop=merge's input. Powered bypdfcpu.op=rotate— rotate pages clockwise bypdf.Rotationdegrees (a non-zero multiple of 90). Optionalpdf.Pagesselection limits which pages turn. Powered bypdfcpu.op=fillForm— fill interactive form fields frompdf.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 (viaklippa-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 andPrintToPDFthe exchange body (which must be an HTML string or[]byte). Maximum fidelity: Tailwind, custom fonts, complex CSS, embedded charts. Requires achromium/google-chromebinary 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:
stringor[]byte— rendered as a single text block.*NativeDoc/NativeDoc— Go struct.map[string]anywith 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:
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
chromePathto point at a custom location (e.g. inside a container withoutchromium-browseraliased). - Sandboxing —
chromedp.NoSandboxis set; the wrapper is intended to run in trusted environments (typically inside a server container where you control the input HTML). - Data URL encoding —
escapeForDataURLpercent-encodes%and#; the rest of the HTML is passed verbatim. This avoids the URL-fragment trap (#) while keeping the data URL small. nativemode page size — defaults to US Letter; non-defaultpaperWidth/paperHeightinvokesfpdf.NewCustomwith explicit dimensions in inches.- Both modes overwrite the In() body — the next pipeline step sees the PDF bytes, not the HTML / structured input.
op=splitoutput is[][]byte— pair it with asplit:step to fan each chunk out as its own exchange, or amergelater to reassemble.pdf.Span=0splits on top-level bookmarks instead of a fixed page count.op=rotateneeds a multiple of 90 —pdf.Rotationof0or a non-multiple errors rather than silently no-op'ing.90/270/-90swap page width/height;180keeps them.pdf.Pagesis optional (all pages when unset) and accepts ranges ("1-3,5") or a list.op=fillFormfield-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.fieldsSetreports 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=fillFormruns the same embedded pdfium WebAssembly module thatvlm://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=createchrome/native rendering.pkg/component/pdf/ops.go—op=extractText/merge/append/split/rotate.pkg/component/pdf/formfill.go—op=fillForm(pdfium form engine).