Protected reading
@nextgensoftwares/folio-plugin-protect turns <FolioView> into a protected reader, and @nextgensoftwares/folio-render-node renders pages on the server. Read Reader & protection first: it explains what each layer stops, what it can't, and what your server must enforce.
import { canvasPages, FolioProtect } from '@nextgensoftwares/folio-plugin-protect';
import { FolioReaderBar, FolioView } from '@nextgensoftwares/folio-react';
// Once, outside render: a stable page component.
const protectedPages = canvasPages({
painters: composed.exporters.canvas, // watermark (and other layers) drawn into the pixels
forensic: { payload: forensicPayload(`${user.id}:${session}`) },
onTamper: (e) => audit('tamper', e), // { kind, page, at }
beforeRender: (page) => ensureToken(page),
});
<FolioProtect copy="block" print={false} blurOnFocusLoss onCopy={(i) => audit('copy', i)}>
<FolioView ref={view} editor={null} layout={layout} readOnly display="page"
pageComponent={protectedPages} pageLayers={composed.pageLayers} layerVariables={{ user: user.name }}
top={<FolioReaderBar view={view} pageCount={layout.pages.length} />} />
</FolioProtect>Canvas pages
canvasPages(options) returns a component for FolioView's pageComponent prop. Pages are painted offscreen at zoom × devicePixelRatio (capped by maxScale, default 3) and swapped in at once; they re-render on zoom, layer changes (the watermark's setOptions) and colour adapters (dark mode). Virtualization is unchanged: only mounted pages have canvases.
| Option | Meaning |
|---|---|
painters | canvas painters by fragment type: composePlugins(...).exporters.canvas |
math | math typesetter; default: the view's renderMath (KaTeX) drawn as vectors |
images, resolveSrc | image loading (CORS <img>); images without CORS headers draw as placeholders |
forensic | { payload, strength? } invisible dot pattern |
onTamper | (event: TamperEvent) => void |
beforeRender | (pageIndex) => void | Promise<void>; a rejection leaves the page blank |
The guard repairs: re-attaches a removed canvas or page, removes injected nodes, restores inline styles and repaints changed pixels. Kinds: removed, injected, style, resized, hidden, pixels, stylesheet.
Links inside canvas pages aren't clickable, and screen readers get one role="img" per page ("Page N"): protected pages trade accessibility for protection. Offer an accessible format to students who need one.
<FolioProtect>
| Prop | Default | Meaning |
|---|---|---|
copy | 'block' | 'allow', 'block' or { maxChars, budget? } |
editor | read-only editor whose selection is copied (truncated) | |
print | false | allow printing; otherwise print shortcuts are swallowed and content is hidden in print |
select, contextMenu, drag | false | allow text selection, the context menu, dragging |
blurOnFocusLoss | false | blur while the window is unfocused or hidden |
detectDevtools | false | blur while docked devtools seem open (heuristic; false positives) |
onCopy, onBlocked, onTamper | audit hooks |
Server-rendered pages
import { renderPageImage, exportFlattenedPdfNode } from '@nextgensoftwares/folio-render-node';
const img = await renderPageImage(layout, 41, {
fonts: [{ family: 'Geist', data: geist }, { family: 'Noto Sans Arabic', data: noto }],
pageLayers: composed.pageLayers, painters: composed.exporters.canvas,
variables: { user: 'Student 42', timestamp }, forensic: { payload }, dpi: 144, format: 'webp',
});<FolioImagePager pageCount pageSize pageSrc> displays them with the same display modes, lazy loading and neighbour prefetch, drawing each image into a canvas (asCanvas, default on).
@nextgensoftwares/folio-render-node uses @napi-rs/canvas (MIT, prebuilt Skia binaries, no system libraries) and shares its drawing code with the browser through @nextgensoftwares/folio-render-canvas, so the browser bundle never includes it. By default it only loads data: image URLs (no fetching URLs from document content on the server); pass images to read your storage.