Headless rendering, search, and selection
A headless DocumentViewer has the same parser, render, search, selection, cancellation, and resource-limit behavior as an attached viewer, but creates no DOM viewport.
Render into a canvas
import { ViewerClient, ViewerError } from "@zrimo/viewer";
const client = ViewerClient.create({
assetBaseUrl: new URL("/zrimo/", location.href),
});
const viewer = client.createViewer();
await viewer.load(bytes, { fileName: "report.pdf" });
const canvas = document.createElement("canvas");
await viewer.renderPage(0, canvas, {
zoom: 1.5,
devicePixelRatio: window.devicePixelRatio,
});
document.body.append(canvas);The caller owns the target canvas. The backend sets its pixel dimensions; zoom is the logical scale and devicePixelRatio controls output density. Page indices are zero-based.
OffscreenCanvas and cancellation
const controller = new AbortController();
const target = new OffscreenCanvas(1, 1);
const pending = viewer.renderPage(3, target, {
zoom: 2,
devicePixelRatio: 1,
signal: controller.signal,
});
controller.abort();
try {
await pending;
} catch (error) {
if (!(error instanceof ViewerError) || error.code !== "aborted") throw error;
}close(), destroy(), and a newer load() also cancel active renders and reject stale results. An external signal does not take ownership of the viewer.
Thumbnails
const thumbnail = document.createElement("canvas");
await viewer.renderThumbnail(0, thumbnail, {
maxWidth: 180,
maxHeight: 240,
devicePixelRatio: 2,
});Thumbnail scaling never enlarges past the package base scale. Spreadsheet documents expose bounded region rendering instead of whole-sheet thumbnails.
Spreadsheet viewport
const sheetCanvas = document.createElement("canvas");
await viewer.renderSheetViewport(
0,
sheetCanvas,
{ row: 100, column: 20, rowCount: 40, columnCount: 12 },
{
width: 960,
height: 640,
devicePixelRatio: 2,
// Unscaled pixels clipped inside row 100 / column 20:
scrollOffsetX: 18,
scrollOffsetY: 6,
// Optional view-only widths in unscaled CSS pixels, keyed from 1:
columnWidths: { 20: 180, 21: 240 },
},
);Rows and columns in both SpreadsheetViewportRange and CellRange are one-based spreadsheet coordinates; sheetIndex remains zero-based. The attached SpreadsheetViewport calculates this range and the partial-cell offsets from sparse row/column geometry. A custom headless scroller should use the same convention; offsets do not include frozen panes or headers. Width overrides affect only rendering and never modify the source workbook.
Search without UI
const { matches } = await viewer.search("日本語");
for (const match of matches) {
console.log(match.pageIndex, match.start, match.end, match.text);
}
const pageText = await viewer.getPageText(0);Matches point into the original logical UTF-16 text even when NFKC/case folding changed the search representation. Search is literal, not fuzzy, and preserves Arabic diacritics.
Programmatic selection and copy
await viewer.selectText({
startPageIndex: 0,
startOffset: 10,
endPageIndex: 1,
endOffset: 24,
});
const text = await viewer.copySelection();
viewer.selectCells({
sheetIndex: 0,
startRow: 2,
startColumn: 1,
endRow: 8,
endColumn: 4,
});
viewer.selectCellRanges([
{ sheetIndex: 0, startRow: 2, startColumn: 1, endRow: 2, endColumn: 1 },
{ sheetIndex: 0, startRow: 2, startColumn: 3, endRow: 2, endColumn: 3 },
]);
const tsv = await viewer.copySelection();The returned string does not depend on Clipboard permission. Text follows backend logical order rather than visual RTL/LTR placement; cell selection serializes rows as TSV and expands merged ranges.
Cleanup
await viewer.close(); // reusable viewer, status becomes idle
await viewer.destroy(); // terminal and idempotent
await client.destroy(); // destroys any viewers still owned by the client