@ogcio/pdf-export
v1.0.11
Published
PDF export utilities for form submissions
Maintainers
Keywords
Readme
@ogcio/pdf-export
PDF export utilities for form submissions.
npm install @ogcio/pdf-export [email protected]react is also required if you use the React hook (@ogcio/pdf-export/react).
Entrypoints
| Entrypoint | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| @ogcio/pdf-export | Core API — transformers, generatePdf, the browser downloadPdf, and the server-side renderV2SubmissionPdf. No React dependency. |
| @ogcio/pdf-export/react | React hook (useDownloadPdf) plus everything from the core entrypoint. Marked 'use client'. |
Fonts
This package bundles the Lato font family (regular and bold). Font files are lazy-loaded via dynamic import and only fetched when the first PDF is generated.
The bundled subset covers Latin (including Latin Extended), Greek and Cyrillic. Characters outside those blocks — CJK, Arabic, Hebrew and other complex scripts — render as blank glyphs.
There is no support for loading custom fonts through this package.
Options reference
PdfOptions (passed to downloadPdf / generatePdf / download)
| Option | Type | Default | Description |
| ------------ | --------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| showTime | boolean | true | When true, the "Time submitted" field shows both date and time. When false, shows date only. |
| filename | string | Auto-generated | Custom filename for the downloaded PDF. Auto-generated from title and submission ID if omitted. |
| styling | object | — | Advanced layout overrides (page size, margins, colours, font sizes). See PdfStylingSchema for details. |
| truncation | object | — | Override display truncation limits for field labels and values. See truncation limits below. |
Truncation limits
Text is truncated before rendering to keep PDFs readable. There are three layers:
- Zod schema (input gate): per-field hard ceiling for labels and the title — inputs exceeding it are rejected outright. Field values have no per-field ceiling; they are truncated for display instead
- Display truncation (default): what actually renders in the PDF; configurable via
truncation - Aggregate content cap: the combined characters across the title and every field label and value is capped (~1,000,000) and rejected before layout, bounding total work regardless of any single field's length
| Field | Zod limit | Display default | Configurable | | ------------ | --------- | -------------------------------- | -------------------------------------------- | | Field labels | 10,000 | 5,000 graphemes | Yes | | Field values | None† | 100,000 graphemes (~40 A4 pages) | Yes | | Title | 1,000 | 200 graphemes | No — matches the 200-character DB constraint |
† Field values carry no per-field Zod ceiling: an oversized single answer is truncated to the display limit for its verified owner rather than rejected. Total content is bounded by the aggregate cap above.
await downloadPdf(pdfData, {
truncation: {
labelLength: 10_000, // max 10_000
valueLength: 150_000, // display truncation for values
},
})Truncated text is appended with …. Labels and the title above their Zod limit are rejected at runtime with a validation error; oversized field values are truncated instead, and submissions above the aggregate content cap are rejected.
Transformer options (V1Options / V2Options, passed to transformV1ToPdfData / transformV2ToPdfData)
| Option | Type | Default | Description |
| --------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| excludeAttachments | boolean | false | When true, attachment fields are omitted from the PDF output. |
| includeFieldNumbers | boolean | true | When true, field labels are prefixed with sequential numbers. |
| includeSections | boolean | true | When true, section headers are included in the PDF output. |
| includeStatements | boolean | false | When true, statement fields are included and section descriptions are shown. When false, statements are excluded and section descriptions are hidden. |
| virusScanMessages | object | Built-in | Map of scan status codes to display messages for attachment fields. Defaults to VIRUS_SCAN_STATUS_EMAIL_MESSAGES from @formsg/shared. |
Form Logic & Visibility
The transformers (transformV1ToPdfData and transformV2ToPdfData) automatically handle field visibility based on form logic if formLogics is provided in the input object.
- Field Filtering: If a field is hidden by logic (e.g., a "Show" rule whose conditions are not met), it will be omitted from the PDF.
- Section Filtering: If all fields within a section are hidden or unanswered, the section header itself will be automatically removed to keep the PDF clean.
- Manual Visibility: The transformers also respect the
isVisibleproperty on individual fields/responses if present.
To enable this, ensure your input object includes the formLogics array:
const pdfData = transformV2ToPdfData({
// ... other input fields
formFields: form.form_fields,
formLogics: form.form_logics, // Enables automatic visibility filtering
content: submission.content,
})Usage
The examples below use
@ogcio/pdf-export— useworkspace:*in monorepo consumers.
Environment support
The core entrypoint runs both in the browser and on the server; only the React hook and the DOM download helper are browser-bound. Import accordingly:
| Export | Runs where | Notes |
| ---------------------------------------------- | --------------------- | ----------------------------------------------------------- |
| useDownloadPdf (@ogcio/pdf-export/react) | Browser only | Client component; the module is marked 'use client' |
| downloadPdf | Browser only | Triggers a file download through the DOM |
| generatePdf, renderV2SubmissionPdf | Browser or server | Pure render, no DOM — safe in a Node route handler / worker |
| transformV1ToPdfData, transformV2ToPdfData | Browser or server | Pure data transforms |
renderV2SubmissionPdf is the server render pipeline pii-server uses to stream a submission PDF to the citizen who owns it (see Server-side rendering below).
React hook
'use client'
import { useDownloadPdf, transformV1ToPdfData } from '@ogcio/pdf-export/react'
export function DownloadButton({ data }) {
const { download, isGenerating } = useDownloadPdf({
onError: (err) => console.error(err),
onSuccess: () => console.log('Done'),
})
const onClick = async () => {
const pdfData = transformV1ToPdfData(data, {
excludeAttachments: true,
})
await download(pdfData, {
showTime: false,
})
}
return (
<button onClick={onClick} disabled={isGenerating}>
Download PDF
</button>
)
}Direct (non-React) usage
import { downloadPdf, transformV2ToPdfData } from '@ogcio/pdf-export'
const pdfData = transformV2ToPdfData(input, {
excludeAttachments: true,
includeStatements: false,
})
await downloadPdf(pdfData, {
showTime: true,
filename: 'submission.pdf',
})Server-side rendering
renderV2SubmissionPdf runs the full transform → generate → bytes pipeline in a single call and returns the raw PDF bytes plus a sanitised filename. It has no DOM dependency, so it is safe inside a Node route handler or worker. The caller supplies submittedAtFormatted (rendered verbatim), keeping locale and timezone formatting under the server's control rather than the library's.
import { renderV2SubmissionPdf } from '@ogcio/pdf-export'
const { bytes, filename } = await renderV2SubmissionPdf({
input: {
title: form.title,
submissionId,
submittedAt: new Date(submittedAt),
locale, // e.g. 'en-IE' | 'ga-IE'
formId,
baseUrl,
content, // Record<string, string | string[]>
formFields,
formLogics,
},
submittedAtFormatted, // pre-formatted for the submitter's locale/timezone
})
reply.header('Content-Type', 'application/pdf')
reply.send(Buffer.from(bytes)) // Node: wrap the Uint8ArraytransformOptions (V2Options) and pdfOptions (PdfOptions) are also accepted, feeding the transform and generation stages respectively. The function throws V2TransformError on invalid transformer input and PdfGenerationError (carrying a PdfErrorCode, e.g. INVALID_TITLE) on validation or render failure, so callers can map failure classes without string-matching (see Error handling).
Error handling
The transform and render pipeline throws typed errors, so callers can classify a failure by class rather than string-matching a message:
V2TransformError— the submission content failed the transformer's input schema. A permanent property of the stored data.PdfGenerationError— carries aPdfErrorCode:
| PdfErrorCode | Meaning | Nature |
| ----------------------- | ---------------------------------------------- | ----------------- |
| INVALID_INPUT | A field or label failed validation | permanent (data) |
| INVALID_TITLE | Title empty or over the cap | permanent (data) |
| INVALID_SUBMISSION_ID | Malformed submission id | permanent (data) |
| INVALID_FORM_URL | Malformed form URL | permanent (data) |
| INVALID_DATE | Unparseable submitted-at date | permanent (data) |
| CONTENT_TOO_LARGE | Aggregate content over the ~1,000,000-char cap | permanent (data) |
| GENERATION_FAILED | pdfmake render fault | transient (fault) |
| EMPTY_RESULT | pdfmake produced no bytes | transient (fault) |
| INIT_FAILED | pdfmake or font initialisation failed | transient (fault) |
| TIMEOUT | Render exceeded the configured timeout | transient (fault) |
| NOT_BROWSER | downloadPdf called outside a browser | usage (browser) |
| DOWNLOAD_FAILED | Browser file download failed | fault (browser) |
V2TransformError and the permanent (data) PdfErrorCodes describe input that no retry can fix, so a consumer should surface them as a permanent failure (a 4xx). The render faults warrant a 5xx and a retry. The two browser codes only arise from downloadPdf in the browser. pii-server maps the server-path codes this way when streaming a citizen's PDF.
Lazy loading
All heavy assets are lazy-loaded via dynamic import() on first use:
- pdfmake library — loaded when
generatePdf/downloadPdfis first called - Lato font files — loaded on first PDF generation
The pdfmake instance is cached after first initialisation, so subsequent calls are fast. Font assets are also cached once loaded.
For the lightest initial page load, trigger PDF generation on user action (e.g. button click) rather than on render.
Releasing
When this package is built, it copies code from @formsg/shared into dist. That means the npm package can go stale after a shared-code change even when nothing in packages/pdf-export changed.
CI checks whether this package needs a version bump. On pull requests, it builds @formsg/shared and @ogcio/pdf-export for both the PR and the target branch, then compares the built package output.
- Unchanged: no version bump is allowed.
- Changed:
packages/pdf-export/package.jsonmust have a higher unpublished version than the target branch, or CI fails.
Publishing is automatic after CD successfully deploys DEV from develop. CD trusts the CI check, rebuilds the package, skips if the committed version is already on npm, and otherwise publishes it with pnpm publish. A manual pipeline remains as a fallback and uses the same publish template.
Security notes
This package includes built-in safeguards:
- Input validation via Zod schemas
- Text sanitisation (control characters removed)
- Filename sanitisation before download
- Size/length limits on key input fields
Consumers should still treat transformer input as untrusted data.
