Rendering
The render plugin draws the pages. Put <RenderLayer> in each page, and the plugin decides what
to render, how big and when, keeps the results cached, and redraws only the pages whose content
changed:
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { localEngine } from '@embedpdf/engine';
import './basic.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<Stage class="stage">
<RenderLayer />
</Stage>
</DocumentGate>
</Viewer>- Sharp text. Pages render at exactly the pixels your screen needs, so small text stays crisp, even on a low-resolution display.
- A memory ceiling. A whole-page picture is never wider than 640 device pixels. Zooming in further uses small tiles for the part you can see, so deep zoom never asks for a giant image.
- Caching. Scrolling away and back is instant, and two views of the same page share one render.
Sharp at every zoom#
A page shows a whole-page picture first, which appears fast. When you zoom in past what that picture can show sharply, tiles take over for the part you can see:
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { localEngine } from '@embedpdf/engine';
import ZoomBar from './ZoomBar.svelte';
import '../deep-zoom.css';
const engine = localEngine();
// Open at 250%: past what a whole-page picture shows sharply, so tiles carry it.
const plugins = [stagePlugin({ zoom: { level: 2.5 } }), renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<ZoomBar />
<Stage class="stage">
<RenderLayer />
</Stage>
</DocumentGate>
</Viewer>Try it: it opens at 250%, past what a whole-page picture shows sharply. Zoom in further: only the tiles you can see load, the area under the pointer sharpens first, and when you stop, the tiles redraw at exactly your zoom.
While you pinch, the pixels you have scale along, so nothing reloads mid-gesture. About 150 milliseconds after the zoom comes to rest, the page redraws at the new size. You configure none of this.
Set the memory budget#
One setting decides how big a whole-page picture may get, in device pixels:
renderPlugin({ fullPage: { maxWidth: 1280 } });The default, 640, makes the first picture of each page small and fast, and lets tiles carry the
sharpness at normal reading zooms. A bigger budget means fewer tiles but a slower first paint and
more memory. For a view that should never tile, such as a thumbnail strip, set tiles to false on
its layer.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { localEngine } from '@embedpdf/engine';
import BudgetControls from './BudgetControls.svelte';
import '../budget.css';
const engine = localEngine();
// Zoomed in, where the budget and the tiles show.
const plugins = [stagePlugin({ zoom: { level: 2 } }), renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<BudgetControls />
<Stage class="stage">
<RenderLayer />
</Stage>
</DocumentGate>
</Viewer>Try it: turn the tiles off, then pick a smaller budget. At 200% the page goes soft: the whole-page picture is all there is.
What the picture draws#
A page’s picture draws the page with its annotations and its form fields, each when the user may read it: the document as it is, right for thumbnails and read-only views. A layer on the page that paints a part itself takes that part out of the picture: an annotation layer the annotations, so people can select and change them, and a form layer the form fields. You write nothing for it:
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { interactionPlugin } from '@embedpdf/svelte/interaction';
import { AnnotationLayer, annotationPlugin } from '@embedpdf/svelte/annotation';
import { localEngine } from '@embedpdf/engine';
import AddRectangle from './AddRectangle.svelte';
import '../annotations.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), annotationPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
type Painter = 'picture' | 'layer';
</script>
<script lang="ts">
let painter = $state<Painter>('picture');
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<AddRectangle />
<div class="toolbar">
<div class="segmented" role="group" aria-label="Who draws the annotations">
<button
type="button"
aria-pressed={painter === 'picture'}
onclick={() => (painter = 'picture')}
>
In the page picture
</button>
<button type="button" aria-pressed={painter === 'layer'} onclick={() => (painter = 'layer')}>
Drawn by the annotation layer
</button>
</div>
<p class="hint">
{painter === 'picture'
? 'Part of the picture: it can’t be picked up.'
: 'Left out of the picture: click it, then drag it.'}
</p>
</div>
<Stage class="stage">
{#if painter === 'picture'}
<RenderLayer />
{:else}
<RenderLayer />
<AnnotationLayer />
{/if}
</Stage>
</DocumentGate>
</Viewer>To decide yourself, set annotations or formFields on the render layer: false leaves the part
out, true draws it. A clean preview without the annotations:
<script lang="ts">
import { RenderLayer } from '@embedpdf/svelte/render';
import { Stage } from '@embedpdf/svelte/stage';
</script>
<Stage>
<RenderLayer annotations={false} />
</Stage>Render a page yourself#
For your own preview, an export or a page picker, render any page on demand:
const image = await render.renderPage(page.ref, { width: 800 });
const { url, revoke } = await image.objectUrl();
// …show it, and call revoke() when you're donerenderPage renders exactly the size you ask for, at width pixels or at a scale, and shares the
layers’ cache. For many pages at once, use renderPages, which renders a few at a time:
const thumbnail = await render.renderThumbnail(page.ref, { maxWidth: 200 });
const { applied } = await render.renderPages(
pages.map((page) => page.ref),
{ width: 300 },
);| Option | What it is | Default |
|---|---|---|
width | The width of the image, in pixels | — |
scale | Or a scale: 1 is the page’s size in points | 1 |
includeAnnotations | Draw the annotations into the image | when the user may read them |
includeFormFields | Draw the form fields into the image | false without annotations; else when the user may read the form |
format | 'png', 'webp' or 'bmp' | per engine |
quality | For WebP, from 0 to 1 | per engine |
concurrency | For renderPages, how many pages render at once | 4 |
Asking for annotations or form fields the user may not read fails with permission-denied.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { renderPlugin } from '@embedpdf/svelte/render';
import { localEngine } from '@embedpdf/engine';
import PageImages from './PageImages.svelte';
import '../render-page.css';
const engine = localEngine();
// No Stage: these pictures are plain images, rendered on demand.
const plugins = [renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<PageImages />
</DocumentGate>
</Viewer>When a page changes#
A rendered page is a snapshot, so the plugin watches the document and redraws exactly the pages an edit touched, whether you, your code or someone else on a shared document made it. Nothing redraws while someone is still dragging: it redraws once, when the change is saved.
For a change the plugin can’t see, tell it:
render.invalidate({ pages: [page.ref] }); // redraw these pages
render.invalidate({ pages: [page.ref], scope: 'annotations' }); // only their annotations changed
render.invalidate({ pages: [page.ref], scope: 'fields' }); // only their form fields changedYour own long-lived renders can keep up the same way: render.getRenderEpoch(page.ref) changes
whenever the page’s pixels do.
Loading live preview…
<script lang="ts" module>
import { DocumentGate, Viewer, type OpenInput } from '@embedpdf/svelte/runtime';
import { RenderLayer, renderPlugin } from '@embedpdf/svelte/render';
import { Stage, stagePlugin } from '@embedpdf/svelte/stage';
import { localEngine } from '@embedpdf/engine';
import Redraws from './Redraws.svelte';
import '../redraws.css';
const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin()];
const ebook = async (): Promise<OpenInput> => {
const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>
<Viewer {engine} {plugins} initialDocuments={[{ source: ebook }]}>
<DocumentGate>
{#snippet fallback()}
<p class="loading">Loading…</p>
{/snippet}
<Redraws />
<Stage class="stage">
<RenderLayer />
</Stage>
</DocumentGate>
</Viewer>Methods#
Everything useRender() gives you.
| Method | What it does |
|---|---|
renderPage(page, options?) | Renders one page at exactly the size you ask for, sharing the layers’ cache. Resolves an image; image.objectUrl() gives a URL for an <img>. Render a page yourself |
renderThumbnail(page, { maxWidth }) | Renders a small picture of a page, maxWidth pixels wide |
renderPages(pages, options?) | Renders several pages, a few at a time. Resolves { applied, failed }: each page with its image, in the order you gave, and the pages that failed with why. |
invalidate({ pages?, scope? }) | Redraws pages after a change the plugin can’t see; without pages, every page. When a page changes |
getRenderEpoch(page, layers?) | A number that changes whenever the page’s pixels do, for a picture that draws what layers says (includeAnnotations, includeFormFields, as above). Key your own long-lived renders on it. |
getRenderPolicy() | The sizes the engine renders best at: any size on the local engine, a short list of widths on a CloudPDF server. See Render policy. |
canRender() | Whether page images are allowed. Permissions |
getSettings(), updateSettings(changes), resetSettings() | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use useRenderSettings(). Settings |
Every method that returns a promise also takes a signal in its options, to cancel it.
Settings#
Set them when you register the plugin, and change them while the app runs with
render.updateSettings({ … }); render.resetSettings() goes back to what you registered, and
useRenderSettings() gives you the current settings.
| Setting | What it does | Default |
|---|---|---|
fullPage.maxWidth | The widest a whole-page picture may be, in device pixels | 640 |
fullPage.quantize | Render exact sizes, or snap to a few reusable sizes | 'exact' |
tiles | false turns tiles off everywhere | on |
tiles.size | A tile’s edge, in device pixels | 512 |
tiles.maxScale | The sharpest tiles get; zoom beyond it stretches them | 128 |
tiles.prefetch.margin | How far around the view to load tiles, as a share of the view | 0.5 |
tiles.prefetch.velocityBias | Load further ahead in the direction you’re scrolling | true |
tiles.settleMs | How long a zoom rests before tiles redraw | 150 |
format | 'png', 'webp', or 'bmp' (fastest, local engine only) | per engine |
quality | WebP quality, from 0 to 1 | per engine |
debug | Log what the plugin renders, and why, to the console | false |
<RenderLayer> takes three props: annotations and formFields, unset by default
(see What the picture draws), and tiles (default true).
Events#
<script lang="ts">
import { RenderLayer, useRenderEvent } from '@embedpdf/svelte/render';
import { Stage } from '@embedpdf/svelte/stage';
import { refreshMyPreviews } from './previews';
useRenderEvent(
(render) => render.onInvalidated,
({ pages }) => refreshMyPreviews(pages),
);
</script>
<Stage>
<RenderLayer />
</Stage>| Event | Fires when | Carries |
|---|---|---|
onInvalidated | Pages’ pixels changed and they will redraw | pages, scope, origin |
onSettingsChanged | The settings changed | settings, changed |
Permissions#
| Permission | What it allows | Check |
|---|---|---|
doc.render | Page images, thumbnails and page sections | render.canRender() |
Without it, pages keep their size and place, so the Stage still scrolls and the other plugins still work, but nothing is painted.
Good to know#
Two layers of pixels#
Each page paints from a whole-page picture, capped at the budget, and tiles over the visible part when the view needs more pixels than the budget allows. The whole-page picture is also what you see while tiles load, so a page never goes blank mid-zoom. Each tile is a square of the same size, so a screen of tiles costs about the same at any zoom.
Local and cloud render sizes#
The local engine renders exact sizes: every render is private to this browser, so it asks for precisely the pixels on screen.
Faster rendering#
Each image is compressed before it reaches the page: PNG on the local engine by default. If
profiling shows compression matters, format: 'bmp' skips it on the local engine, at the cost of
memory (a 512-pixel tile is about 1 MB, against tens of KB as WebP).
The engine underneath#
The plugin renders through the engine’s page rendering; see Rendering for page sizes and what a render costs.
Your feedback goes directly to the documentation team.