PDF Export
Exports a flat range of canvases as a browser-generated PDF, with one PDF page per selected canvas.
Feature summary:
- range-based export from the plugin panel
- one PDF page per selected canvas
- optional static or dynamic consumer-provided download filename
- optional cover sheet with consumer-provided label/value metadata
- selectable OCR text when the canvas exposes IIIF OCR annotations
- configurable browser image request settings for public or authenticated image services
- optional consumer callback for the currently selected start and end canvases
By default, PdfExportPlugin uses:
- an automatically generated filename based on the manifest and selected canvas range
- no cover sheet
- public-friendly image fetching with
credentials: "same-origin" - OCR text embedding only when suitable IIIF OCR annotations are present
Setup
Basic Usage
PdfExportPlugin is the default, preconfigured export; add it like any plugin
(see using plugins). For a configured
instance, call createPdfExportPlugin(...) and pass the result instead — in the
browser registry, the factory is exposed on the plugin the registry returns.
Configuring The Plugin
Use createPdfExportPlugin(...) when you want a custom filename, a cover sheet, a specific OCR annotation source, export-only OCR overlays, or custom image request behavior.
import { createPdfExportPlugin } from '@triiiceratops/plugin-pdf-export';
const pdfExportPlugin = createPdfExportPlugin({
getFilename: ({ manifestLabel, startIndex, endIndex, defaultFilename }) =>
manifestLabel
? `${manifestLabel}-${startIndex + 1}-${endIndex + 1}.pdf`
: defaultFilename,
coverSheet: {
title: 'Digitization Summary',
fields: [
{ label: 'Repository', value: 'Example Library' },
{ label: 'Call Number', value: 'MS 123' },
],
},
ocrAnnotationSource: 'https://example.org/canvas/1/ocr',
async getCanvasOcrOverlays({ canvasId }) {
const response = await fetch(
`/api/ocr-overlays?canvas=${encodeURIComponent(canvasId)}`,
);
if (!response.ok) {
return [];
}
const overlays = await response.json();
return overlays.map((overlay: Record<string, unknown>) => ({
...overlay,
// Use 'image' when your OCR API returns coordinates in the
// selected source image's pixel space instead of canvas pixels.
coordinateSpace: 'image',
}));
},
imageRequest: {
credentials: 'same-origin',
},
onSelectionChange({ startCanvas, endCanvas, startIndex, endIndex }) {
console.log('Selected PDF export range', {
startCanvas,
endCanvas,
startIndex,
endIndex,
});
},
});
For script-tag/web component hosts, use the factory exposed on the IIFE plugin global when you need configuration callbacks:
<script>
viewer.plugins = [
window.Triiiceratops.plugins.get('@triiiceratops/plugin-pdf-export').createPdfExportPlugin({
onSelectionChange({
startCanvas,
endCanvas,
startIndex,
endIndex,
}) {
console.log('Selected PDF export range', {
startCanvas,
endCanvas,
startIndex,
endIndex,
});
},
}),
];
</script>
For image services that cannot be fetched directly by the browser, you can also provide a custom image loader:
import { createPdfExportPlugin } from '@triiiceratops/plugin-pdf-export';
const pdfExportPlugin = createPdfExportPlugin({
loadImageBlob: async ({ imageUrl }) => {
const response = await fetch(
`/api/pdf-image?url=${encodeURIComponent(imageUrl)}`,
);
if (!response.ok) {
throw new Error('Unable to load image for PDF export.');
}
return response.blob();
},
});
You can use that configured plugin in either a Svelte app or a bundler-based host app that assigns plugins to the web component:
import { createPdfExportPlugin } from '@triiiceratops/plugin-pdf-export';
const pdfExportPlugin = createPdfExportPlugin({
coverSheet: {
title: 'Export Summary',
fields: [{ label: 'Collection', value: 'Example collection' }],
},
});
viewer.plugins = [pdfExportPlugin];
Configuration shape:
type PdfExportConfig = {
filename?: string;
getFilename?: (context: {
manifestId: string | null;
manifestLabel?: string | null;
startIndex: number;
endIndex: number;
indices: number[];
canvases: any[];
exportedCount: number;
failedCanvases: string[];
defaultFilename: string;
}) => Promise<string | null | undefined> | string | null | undefined;
coverSheet?: {
title?: string;
fields: { label: string; value: string }[];
};
ocrAnnotationSource?: string;
ocrPlacementMode?: 'fit-box' | 'word-anchor';
ocrSizingMode?: 'fit-box' | 'height-only';
ocrVisibilityMode?: 'transparent' | 'invisible' | 'debug';
onSelectionChange?: (selection: {
startIndex: number | null;
endIndex: number | null;
startCanvas: any | null;
endCanvas: any | null;
}) => void;
getCanvasOcrOverlays?: (context: {
manifestId: string | null;
canvasId: string;
canvas: any;
canvasIndex: number;
}) =>
| Promise<
| {
text: string;
x: number;
y: number;
width: number;
height: number;
coordinateSpace?: 'canvas' | 'image';
}[]
| null
| undefined
>
| {
text: string;
x: number;
y: number;
width: number;
height: number;
coordinateSpace?: 'canvas' | 'image';
}[]
| null
| undefined;
imageRequest?: {
credentials?: RequestCredentials;
headers?: HeadersInit;
mode?: RequestMode;
referrerPolicy?: ReferrerPolicy;
};
loadImageBlob?: (params: {
canvas: any;
canvasId: string;
imageUrl: string;
manifestId: string | null;
targetWidth: number;
imageRequest: RequestInit;
resolvedImage: any | null;
}) => Promise<Blob> | Blob;
};
Filename
Set filename when the consuming application should control the downloaded PDF name with a static value. The value is passed directly to the browser download link, so include the .pdf extension when you want it shown in the saved file name.
Set getFilename when the consuming application should compute the downloaded PDF name for each export. The callback receives the manifest identifier and label, normalized selected range, selected canvases, export counts, failed canvas labels, and the generated defaultFilename.
If both filename and getFilename are configured, filename takes precedence and getFilename is not called. If getFilename returns null, undefined, or an empty string, the plugin uses the generated filename fallback.
When both filename and getFilename are omitted, the plugin generates a PDF filename from the manifest label or identifier and the selected canvas range.
Cover Sheet
When coverSheet is configured, the exported PDF begins with a generated summary page.
The cover sheet includes:
- each consumer-provided
label/valuepair - the PDF creation date and time
- the current page URL, when available in the browser
The export UI does not ask end users to edit these fields. They are supplied by the consuming application at plugin creation time.
OCR Support
When a canvas includes IIIF OCR annotations, the plugin embeds selectable text into the exported PDF.
The plugin reads OCR from IIIF annotation data, not from IIIF Search responses. Search hits alone are not enough because the PDF export needs stable text plus canvas-relative bounding boxes.
Manifest OCR annotations are normalized automatically when their xywh boxes are in the selected source image's pixel space instead of the canvas pixel space.
If your app stores OCR outside the IIIF manifest, configure getCanvasOcrOverlays to supply PDF text overlays directly during export. This callback runs only for canvases included in the selected PDF export range. It is not used during normal canvas navigation, search, thumbnail rendering, or viewer startup.
Provider overlay coordinates default to canvas space for backward compatibility. If your provider returns original image pixel coordinates, set coordinateSpace: 'image' on each overlay so the exporter can normalize them before PDF placement.
Supported OCR annotation patterns include:
- IIIF Presentation 3 annotations using
TextualBodyplusmotivation: "supplementing" - legacy IIIF Presentation 2 text annotation lists in
otherContent, includingcnt:ContentAsTextbodies that usesc:paintingfor line text
OCR is resolved in this order during export:
- if
getCanvasOcrOverlaysreturns a non-null value, that result is used and manifest OCR is skipped for that canvas - otherwise, if
ocrAnnotationSourceis set, the plugin loads OCR from that specific annotation page/listid - otherwise, the plugin reads OCR-compatible annotations from every available canvas annotation source
Callback result semantics:
- return
[]to mark the canvas as handled and export it without OCR text - return
nullorundefinedto fall back to manifest-based OCR loading - if the callback throws, the export logs a PDF-scoped warning and falls back to manifest-based OCR loading
OCR rendering options:
ocrPlacementMode: 'fit-box'preserves the existing box-fitting behaviorocrPlacementMode: 'word-anchor'keeps each word anchored to its suppliedxand top-originyposition without vertical recenteringocrSizingMode: 'fit-box'uses both overlay width and height to size the textocrSizingMode: 'height-only'sizes from overlay height only and does not stretch words to fill the OCR widthocrVisibilityMode: 'transparent'uses the existing near-transparent text layer behaviorocrVisibilityMode: 'invisible'prefers PDF invisible text rendering semantics when supported by the PDF layerocrVisibilityMode: 'debug'draws OCR text visibly for placement checks
Default OCR rendering behavior remains backward-compatible:
Recommended settings for word-level OCR overlays:
To make exported PDF text selectable, provide OCR as canvas-linked IIIF annotations with these properties:
- the canvas
widthandheightmust use the same coordinate space as the OCR bounding boxes - each OCR annotation should target a rectangle using
#xywh=x,y,w,hor aFragmentSelectorwithxywh=... - each OCR annotation should use
motivation: "supplementing" - each OCR annotation body should be a
TextualBodywith plain text invalue - embedded and external
AnnotationPageresources are both supported - the original ALTO, hOCR, or other OCR file can also be linked via
seeAlso, but the plugin reads the IIIF annotations directly
Example canvas with an external OCR page:
{
"id": "https://example.org/canvas/1",
"type": "Canvas",
"width": 3000,
"height": 4000,
"annotations": [
{
"id": "https://example.org/canvas/1/ocr",
"type": "AnnotationPage"
}
],
"seeAlso": [
{
"id": "https://example.org/canvas/1/ocr/alto.xml",
"type": "Dataset",
"format": "application/xml",
"label": {
"en": ["ALTO OCR"]
}
}
]
}
Example OCR annotation page:
{
"id": "https://example.org/canvas/1/ocr",
"type": "AnnotationPage",
"items": [
{
"id": "https://example.org/canvas/1/ocr/line/1",
"type": "Annotation",
"motivation": "supplementing",
"body": {
"type": "TextualBody",
"value": "This is one OCR line.",
"format": "text/plain"
},
"target": "https://example.org/canvas/1#xywh=240,380,1620,52"
}
]
}
Tesseract guidance:
- convert each OCR line or word into one IIIF annotation item
- line-level annotations are the easiest starting point for usable PDF text selection, but word-level overlays are also supported
- if your Tesseract boxes are in raw image pixels, make sure the canvas
widthandheightmatch that same pixel space, or scale the boxes during annotation generation
Image Request Notes
The plugin fetches canvas images with credentials: "same-origin" by default. This avoids common CORS failures on public IIIF servers that respond with Access-Control-Allow-Origin: *.
For IIIF-backed exports, the plugin automatically requests wide or spread canvases by height instead of width so landscape pages are not capped by the viewer-width-derived export size. Portrait and square canvases continue to use width-constrained requests.
For IIIF Image API level 0 services, the plugin prefers the painting body's declared image URL instead of synthesizing an arbitrary sized IIIF image request. This is more compatible with services that expose only a fixed image URL alongside a level 0 service description.
Some IIIF image services block browser access entirely. Typical symptoms are:
- no
Access-Control-Allow-Originresponse header 401or403responses for cross-origin image fetches- browser errors such as
TypeError: Failed to fetch
In those cases, a purely client-side export is not possible from a different origin. Configure loadImageBlob so your application can fetch the image through a same-origin proxy, a backend endpoint, or another authenticated path that the browser is allowed to read.
If your image service requires cookies or another authenticated browser session, configure the plugin explicitly:
import { createPdfExportPlugin } from '@triiiceratops/plugin-pdf-export';
const plugin = createPdfExportPlugin({
imageRequest: {
credentials: 'include',
mode: 'cors',
headers: {
Authorization: 'Bearer <token>',
},
},
});
Supported imageRequest fields are passed directly to fetch(...):
credentialsheadersmodereferrerPolicy
Use loadImageBlob when imageRequest is still not enough because the remote service does not allow browser access at all.
Only use credentials: "include" when the IIIF image service is configured for credentialed CORS.