Integration

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

ScenarioChooseWhy
A PDF your backend already produces (invoice, report, contract)PDFServer-decided layout, vector passthrough, crisp
Photos, scans, anything already renderedImagesOne image is one page, fitted without stretching
Labels, receipts, price tagsTemplatesMillimetre precision, {{placeholder}} data binding, built in the online designer
An existing vue-plugin-hiprint templateHiprintConverted by the adapter, see Hiprint templates
A layout you already have as a web pageHTMLSanitised 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.

PDF

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') gives INVALID_REQUEST; a partial overflow is clamped with PAGE_RANGE_CLAMPED; selecting nothing gives INVALID_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 pages

Images 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_UNAVAILABLE rather 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.

See also

On this page