Content sources
How to print PDFs, images, templates and web pages, which to choose, data binding and remote assets.
Last updated
The content of a print job is source.items: an ordered array where the order is
the page order. A single PDF is an array of length 1.
source: {
items: [
{ type: 'pdf', asset },
{ type: 'image', asset, pageLayout: { mode: 'custom', scalePercent: 80 } },
{ type: 'document', document: doc, data: { orderNo: 'A-1024' } },
],
}You rarely write this by hand — compose()'s addPdf / addImage /
addDocument generate it for you.
Which one to choose
| Scenario | Choose | Why |
|---|---|---|
| A PDF your backend already produces (invoice, report, contract) | Server-decided layout, vector passthrough, crisp | |
| Photos, scans, anything already rendered | Images | One image is one page, fitted without stretching |
| Labels, receipts, price tags | Templates | Millimetre precision, {{placeholder}} data binding, built in the online designer |
| An existing vue-plugin-hiprint template | Hiprint | Converted by the adapter, see Hiprint templates |
| A layout you already have as a web page | HTML | Sanitised and snapshotted into pages |
Uploading content
PDFs, images and web pages must first give their bytes to PrintLink:
const asset = await client.uploadAsset(pdfBytes, 'application/pdf');The returned AssetRef can be reused across calls: previewing and then printing
the same PDF, or printing it to several printers, needs no re-upload.
compose() handles uploads for you and deduplicates by SHA-256.
await client.compose({ queueId })
.paper('A4')
.addPdf(pdfBytes) // File / Blob / Uint8Array / an uploaded AssetRef
.printAndWait();.pageRange('2-4') selects pages:
- It is a filter, not a reorder.
'3,1'still produces pages 1 and 3 in order. - To reorder, add the same asset as multiple items:
addPdf(a).pageRange('3').addPdf(a).pageRange('1')(uploaded once). - Bad syntax or a reversed range (
'3-1') givesINVALID_REQUEST; a partial overflow is clamped withPAGE_RANGE_CLAMPED; selecting nothing givesINVALID_REQUEST.
A lone PDF is passed through
With exactly one PDF, PrintLink hands the file to the print queue unchanged, with no re-encoding: the artefact hash matches the file itself.
Images
An image has no paper size of its own — the job-level paper decides, and the
image is fitted into the printable area.
await client.compose({ queueId })
.paper('6in')
.addImage(photoBlob)
.printAndWait();For precise placement use .imageLayout(...), which affects only that image —
see print options.
When a page consists of a single full-bleed image, PrintLink treats it as a photo page: the original pixels are embedded directly (JPEGs are not re-encoded) and the sheet orientation follows the photo. This is the sharpest path for photos.
Templates
Describe the page with structured elements rather than HTML layout.
{{placeholders}} are replaced with data before printing:
const doc = PrintLink.documentBuilder()
.page('100x150', (page) =>
page
.text({ x: 5, y: 6, width: 90, height: 10, text: 'Order: {{orderNo}}' })
.qrcode({ x: 60, y: 20, width: 30, height: 30, value: '{{orderNo}}' }),
)
.build();
await client.compose({ queueId })
.paper('100x150')
.addDocument(doc).data({ orderNo: 'SO-2026-0001' })
.printAndWait();Data travels with the content item: adding the same template N times with N sets of data produces N pages in one job, with no interference:
const job = client.compose({ queueId }).paper('100x150');
for (const order of orders) job.addDocument(template).data(order);
await job.printAndWait();Placeholders support plain path lookup only ({{order.customer.name}}) and
never evaluate expressions. A path missing from the data is left as-is with a
BINDING_MISSING warning.
Templates also support two data-driven elements, table and repeat, expanded
into basic elements before rendering and continued onto new pages when they
overflow:
{ id: 'lines', type: 'table', x: 8, y: 30, width: 84, height: 40,
rowHeight: 7, dataPath: 'lines',
columns: [
{ key: 'sku', title: 'SKU' },
{ key: 'qty', title: 'Qty', width: 0.6, align: 'right' },
],
header: { repeat: true } } // repeat the header on continuation pagesImages inside a template cannot reference a remote URL: only registered assets
or inline data:image/* are allowed. To bind an image from your data, use the
Image element's sourceBinding field pointing at that path — the value must
still be one of those two forms. Anything else gives BINDING_VALUE_INVALID and
falls back to the static image rather than failing silently.
Web pages (HTML)
Hand PrintLink a piece of HTML and it sanitises and snapshots it into pages:
await client.compose({ queueId })
.paper('A4')
.addHtml(htmlBytes) // an uploaded text/html asset
.printAndWait();Limits:
- A single HTML asset is at most 1 MiB, at most 10 HTML items per request, and at most 50 pages per item.
- Scripts, external references and
file:URLs are all stripped; rendering makes zero network requests. - Environments without a browser engine (headless services) return
HTML_SNAPSHOT_UNAVAILABLErather than degrading to plain text.
Remote assets
Images and web pages can skip the upload and point at a public URL for PrintLink to fetch:
await client.compose({ queueId })
.paper('A4')
.add(item) // { type: 'image', remote: { url: 'https://cdn.example.com/a.png' } }
.printAndWait();Public targets only
For safety, remote fetching accepts public targets only: http(s) on the
default port, no credentials, and the host must not be loopback, private,
link-local or multicast. A private address is refused (REMOTE_FETCH_DENIED)
rather than "filtering out the private ones and connecting to the rest".
Fetched bytes go through the same checks as an upload: allow-list, size cap, and type detection by content. PDFs must still be uploaded — a remote PDF has no form the user can inspect beforehand.