EmbedPDF

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.stateWhat 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:

FieldWhat the engine does with it
displayNameBecomes the annotation’s author, the name PDF apps show, and a form field’s filledByName.
userIdBecomes the annotation’s userId, createdBy and modifiedBy, and a form field’s createdBy and filledBy.
groupIdThe 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'); // false

Permissions 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.

Was this page helpful?

Your feedback goes directly to the documentation team.