Opening documents
engine.open() gives you a handle to one document. Everything you do with the document starts
from that handle.
const response = await fetch('/contract.pdf');
const doc = await engine.open({ kind: 'bytes', bytes: await response.arrayBuffer() });Open a document#
Give the engine the file’s bytes, as an ArrayBuffer or a Uint8Array. Where they come from
doesn’t matter. A file the user picked:
const file = input.files![0];
const doc = await engine.open({ kind: 'bytes', bytes: await file.arrayBuffer() });A file on disk, in Node:
import { readFile } from 'node:fs/promises';
const doc = await engine.open({ kind: 'bytes', bytes: await readFile('contract.pdf') });The engine gives each document an id (doc.id). To choose it yourself, pass id (any input
takes one); it must be unique among the documents open on this engine.
A file that isn’t a PDF, or is too damaged to read, fails with DocOpenFailed.
Password-protected files#
A PDF can have its own password. When you know it, pass it when you open:
const doc = await engine.open(input, { password: 'secret' });When you don’t, open without one and ask the user only if the file needs it.
doc.security.passwordPrompt tells you:
const doc = await engine.open(input);
if (doc.security.passwordPrompt.state === 'required') {
const password = await askUserForPassword();
await doc.security.unlock({ password });
}passwordPrompt.state | What it means |
|---|---|
'none' | Nothing to do: the file has no password, or it’s already fully unlocked. |
'required' | The file can’t be read without its password. Ask before you do anything else. |
'optional' | The file is open, but its owner password would give more rights. You may offer it. |
passwordPrompt.hint says which password to ask for ('user' or 'owner'), when the engine
knows. When the state is 'required', passwordPrompt.incorrect is true if the last password
you tried was wrong.
A locked document still opens: without a password, open() succeeds, and every call that reads
the file fails with DocPasswordRequired until you unlock it. A wrong password doesn’t fail
open() either: the document opens locked, and passwordPrompt.incorrect is true, so you can
say “that password is wrong” and ask again.
unlock() fails with DocPasswordIncorrect when the password is wrong, so you can ask again:
import { EngineError, EngineErrorCode } from '@embedpdf/engine';
while (doc.security.passwordPrompt.state === 'required') {
try {
await doc.security.unlock({ password: await askUserForPassword() });
} catch (error) {
if (!EngineError.is(error, EngineErrorCode.DocPasswordIncorrect)) throw error;
showMessage('That password is wrong. Try again.');
}
}Owner password#
A PDF can have two passwords. The user password opens it; the owner password also lifts the
file’s own restrictions, such as “no printing” or “no copying”. When a file is open but not with
its owner password, passwordPrompt is 'optional'.
To accept only the owner password, pass mode: 'owner'. The user password then fails with
DocPasswordIncorrect:
await doc.security.unlock({ password, mode: 'owner' });The file’s own restrictions only count when the user’s permissions include pdf.permissions;
see Permissions.
Who is working on it#
Tell the engine who the user is and what they may do when you open the document:
const doc = await engine.open(
{ kind: 'bytes', bytes },
{
identity: { userId: 'u_381', displayName: 'Dana Smith', groupId: 'legal' },
scope: [
'doc.open',
'doc.render',
'doc.text.select',
'doc.annotate.read',
'annotations:create:self',
'annotations:update:self',
'annotations:delete:self',
],
},
);identity is who the user is. The engine writes three of its fields into what the user creates or
changes:
| Field | What the engine does with it |
|---|---|
displayName | Becomes the annotation’s author, the name PDF apps show, and a form field’s filledByName. |
userId | Becomes the annotation’s userId, createdBy and modifiedBy, and a form field’s createdBy and filledBy. |
groupId | The group new annotations and form fields go in (groupId). |
The other fields (email, title, organization, organizationalUnit) never go into the PDF.
The viewer uses them in stamps and signature appearances.
scope is what the user may do, as a list of permissions. Leave it out and the user may do
everything; the engine logs a reminder once, so you notice before you ship. Permissions that
name the user’s own annotations (:self) need an identity with a userId to check them
against: without one, open() fails with InvalidArg. A :group=<id> permission names its
group itself, so it needs no identity.
Read both back from doc.security:
doc.security.identity?.displayName; // 'Dana Smith'
doc.security.allows('doc.download'); // falsePermissions lists every permission and how to build an interface that matches them.
Close a document#
Close a document when you’re done with it:
await doc.close();Closing frees the memory the document uses. After it, calls on doc fail with DocNotOpen.
Closing twice is fine.
One engine, many documents#
Create one engine for your app and open every document on it:
const [contract, invoice] = await Promise.all([
engine.open(contractInput),
engine.open(invoiceInput),
]);Keep the engine for as long as your app runs. When you do want to shut it down, at the end of a script for example, destroy it; that also closes every document still open on it:
await engine.destroy();All documents on one engine share one copy of PDFium, so opening another document doesn’t start another worker. Each open document holds its file in memory until you close it.
Good to know#
Layers: keep your changes apart from the file#
The engine can keep your changes apart from the original bytes: downloadLayer() saves just the
changes (a layer), and you open them later on top of the original. A layer holds only what you
changed, so it stays small however large the file is.
// Save only what changed
const changes = await doc.downloadLayer();
// Later: the original, with the changes on top
const reopened = await engine.open({
kind: 'layerBytes',
baseBytes: original,
layer: { kind: 'artifact', bytes: changes },
});Leave out layer (or pass { kind: 'fresh' }) to start with no changes. baseKey is an id for
the original bytes; documents opened with the same baseKey share them in memory.
Large files in Node#
In Node, layerFile opens a file straight from disk. The engine reads the parts it needs when it
needs them, so the file never has to fit in memory:
const doc = await engine.open({ kind: 'layerFile', basePath: '/data/scan-4000-pages.pdf' });The changes can live in a file too: layer: { kind: 'artifact-file', path }. layerFile needs
PDFium’s native build; on the WebAssembly runtime it fails with NotImplemented.
Your bytes after open()#
In the browser, an ArrayBuffer you pass is transferred to the engine, not copied, so it’s empty
afterwards. Pass a copy to keep yours, such as buffer.slice(0).
Cancelling an open#
open() returns a promise you can cancel, like every call:
const opening = engine.open(input);
cancelButton.onclick = () => opening.abort();See Errors & cancelling.
Your feedback goes directly to the documentation team.