Skip to content

PDF-SDK 2.4.1 is out.See what’s new

PDF-SDK
Skip to Content
Docs

Web

The engine runs in the page as WebAssembly and draws into a <canvas> you provide. PDFEditor is the whole API: open bytes, let the user edit, take bytes back. Nothing is uploaded — the document never leaves the tab.

1. Install

The package, @pdfsdk/editor, comes with your license as an npm tarball. Install it like any local package:

npm install ./pdfsdk-editor-2.4.1.tgz

It contains the TypeScript API (dist/, with type declarations) and the engine (public/wasm/pdfeditor.js and pdfeditor.wasm, about 9.6 MB). Copy the wasm/ folder to a path your site serves — by default the editor looks for it at ./wasm/ relative to the page — or pass wasmUrl to point elsewhere.

2. Serve it cross-origin isolated

The engine runs its rendering work on real threads, which needs SharedArrayBuffer. Browsers only allow that on a page that is cross-origin isolated: served over HTTPS (or localhost) with both of these headers on the page and its subresources.

Cross-Origin-Opener-Policy:   same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them PDFEditor.create rejects with an error that says so. Scope the headers to the page that hosts the editor if the rest of your site loads third-party resources — require-corp blocks any cross-origin resource that does not opt in.

HostHow
Netlify, Cloudflare Pagesa _headers file
Vercelheaders in vercel.json
nginxadd_header in the location that serves the editor
GitHub Pagesnot possible — it cannot set response headers
# _headers (Netlify / Cloudflare Pages) — isolate only the editor's path
/editor/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp

Serve .wasm as application/wasm, compressed: brotli cuts the download to about a third, gzip to about 40%.

3. Create an editor and open a PDF

import { PDFEditor, PDFOpenError, PDFLicenseError } from '@pdfsdk/editor';

const canvas = document.querySelector('canvas')!;
const editor = await PDFEditor.create(canvas, {
  licenseKey: 'PE1.eyJ2Ijox…',   // issued for this site's domain
  wasmUrl: '/static/wasm/',        // where pdfeditor.js / .wasm are served (default './wasm/')
  minZoom: 0.5,
});

const bytes = new Uint8Array(await file.arrayBuffer());
try {
  editor.open(bytes);
} catch (err) {
  if (err instanceof PDFOpenError && err.reason === 'password') {
    editor.open(bytes, 0, await askForPassword());
  } else throw err;
}

Size the canvas with CSS; the editor follows it (a ResizeObserver rebuilds the drawing buffer when the element settles). Pointer, wheel and pinch input are handled for you, and holding Space pans. The rest of the keyboard is yours to route, because only your page knows what has focus: call typeText() and backspace() while isEditing, formKey() while a form field has focus, and bind your own shortcuts to undo(), redo(), deleteSelection() and the rest.

4. Tools and properties

The editor is a canvas, not a toolbar — you build the chrome and drive it through the API.

editor.tool = 'highlight';            // arm a tool: 'select', 'text', 'image', 'freehand', 'rect', …
editor.addEventListener('selectionchange', (e) => {
  const { selected, props } = (e as CustomEvent).detail;
  // props: kind, colours, font, size, opacity… — enough to render a properties panel
});
editor.setFillColor(0x0a84ffff);      // colours are 0xRRGGBBAA
editor.setTextStyle(true, null);      // bold on; leave italic as each run has it
editor.undo();

Events the editor dispatches:

EventWhen
selectionchangethe selection or its properties changed
historychangeundo / redo availability changed
pageschangethe page count or the active page changed — on open(), a page turn, or pages added, removed, moved or rotated; detail is { count, active }
zoomchangethe zoom level changed
toolchangethe armed tool changed
editingchangean inline text-editing session began or ended
formfocuschange, fieldselectionchangeform-field focus and selection
markupchange, inkchange, placementchangemarkup, drawing and placement previews
toolrequestthe user tapped or dragged out a spot for a tool that needs something from you first (image, note, sound, signature, stamp, link, page); detail is { tool, at, rect } — supply it, for example with insertImage()
placementrefuseda new text box was refused because the spot is too close to existing text; the tool stays armed
hoverchangethe pointer moved onto or off a selectable object; detail is its box, or null
readonlychangeviewer mode was switched
licensechangea license key was applied while editors exist (setLicenseKey, or create with a licenseKey)

Look and feel

Because the chrome is yours, so is its styling, language and layout. What the engine itself draws is set through these calls. Each one holds for the editor's lifetime, across every open():

CallWhat it changes
canvasColor option · setCanvasColor('#rrggbb')the surround behind the page
minZoom, maxZoom optionsthe zoom range, as multiples of the fitted page: default 0.5–6. maxZoom is clamped to 1–6 and covers pinch, ctrl+wheel and zoomBy; minZoom: 1, maxZoom: 1 turns zoom off. editor.minZoom / editor.maxZoom report the range in force, for greying out your zoom buttons
setDrawPageShadow(on)the drop shadow under the page
setShowAllLayerOutlines(on)dashed hint boxes around every selectable layer
setCornerHandlesOnly(on)corner handles only on a selection
setReadOnly(on)viewer mode: render, zoom, page and search, no editing
setSelectionStyle({ color, handleFill, handleShape, handleScale, borderWidth })the selection box, handles, rotation knob, caret, text-selection band and page guide; handleShape is 'rounded', 'circle' or 'square', handleScale 0.75–1.5, borderWidth 1–3 px
setRotateIcon(imageData | null)the glyph in the rotation knob, drawn as a template in the selection colour (at most 512×512)
setSearchHighlightColors({ match, current })the search highlights' colours; their translucency stays the engine's, so the text under them reads
setNewTextFont(bytes | null, postScriptName)the font of every text box the user places; its size and colour still follow the text around it

Coming from iOS or Android

The iOS and Android editors ship their own toolbars, so they take a configuration that narrows and restyles them. On the web the toolbar is yours, so most of those options are simply how you build it. The ones that reach what the engine draws are the calls above:

iOS / Android optionOn the web
modes, hiddenTools, toolOrder, toolTitles, toolIcons, modeTitles, modeIconsyour toolbar: offer the tools you want, in your order, with your names and icons, and arm one with editor.tool = …
hiddenMenuItems, menuOrder, menuItemTitles, menuItemIcons, headerIconsyour menu and header; each built-in item is one call (searchStart, save, …)
customTools, customMenuItemsyour own buttons; editor.save() returns the document as it stands, every edit included, without writing it anywhere
colorSchemeyour CSS; pass the theme's surround to setCanvasColor when it changes (and setSelectionStyle / setSearchHighlightColors if those differ by theme)
toolDefaultssetMarkupColor / setMarkupOpacity before arming a markup tool, setInkStyle(rgba, width) before the pen; remembering a user's choice is up to you
zoomRange / minZoom, maxZoomthe minZoom and maxZoom options
interfaceFontFamilyyour CSS
fonts, fontsTitle, showsDocumentFonts, showsDeviceFonts, allowsFontImportyour font picker: list your fonts and apply one with setFontData(bytes, postScriptName); embeddedFonts() lists the fonts already embedded on the current page
newTextFontsetNewTextFont
selectionColor, handleFillColor, handleShape, handleScale, selectionBorderWidthsetSelectionStyle
rotateIconsetRotateIcon
searchHighlightColor, searchCurrentHighlightColorsetSearchHighlightColors
signaturePlacementthe placement option of signDocument ('bottom-right' when left out); your sign sheet's picker starts wherever you like
haptics, showsUsageHintsnothing to turn off: the web engine neither vibrates nor shows hints of its own

5. Save

const out = editor.save();   // Uint8Array — a complete PDF, or null on failure
if (out) await upload(new Blob([out], { type: 'application/pdf' }));

Edits are written as real PDF objects. An encrypted document is saved encrypted with the password it was opened with. save() always writes a complete new file, which invalidates an existing digital signature even if nothing was edited — if the user made no changes, keep the original bytes. signDocument appends an incremental update instead, so earlier signatures survive it.

License key

Pass licenseKey to create, or await PDFEditor.setLicenseKey(key, { wasmUrl }) once before the first editor — it loads the engine, so give it the same wasmUrl. Without a valid key for the page's domain, create and open throw PDFLicenseError, whose license.reason says why.

try {
  await PDFEditor.create(canvas, { licenseKey });
} catch (err) {
  if (err instanceof PDFLicenseError) console.error(err.license.status, err.license.reason);
}

Keys, domains, wildcards and development hosts: License keys.

Edit documents from code

Everything a user can do in the editor can be done from code, with no UI: read every layer of a page and its properties, replace text in any script (the layer keeps its font, size, colour and position, and the system's fonts fill in letters its font lacks), restyle, move, resize, rotate and restack layers, add text, images and shapes, mark text up, fill and author forms, edit metadata and pages, search, render a page, and save.

A layer is addressed by its page and an id that keeps naming it while other layers change. Geometry is in page points from the page's top-left corner. Each change is one undo step.

import { PDFDocumentEditor } from '@pdfsdk/editor';

const doc = await PDFDocumentEditor.open(bytes);
for (const block of doc.textBlocks(0)) {
  const text = await translate(block.text ?? '');
  doc.setText(block, text, { shrinkToFit: true });
}
const edited = doc.save();

editor.document is the document an editor is showing: changes appear at once and the user can undo them. Failures throw PDFDocumentError; notFound means the layer is gone, so read the layers again.

The web SDK draws no words of its own (your page builds the toolbar), apart from a new text box's starting text: editor.setNewTextPlaceholder('पाठ').

Digital signatures

import { createDigitalId, loadDigitalId, signDocument, verifySignatures } from '@pdfsdk/editor';

const id = await loadDigitalId() ?? await createDigitalId({ name: 'Ada Lovelace', organization: 'Acme' });
const { bytes } = await signDocument(editor, pdf, id, {
  reason: 'I approve this document',
  placement: 'bottom-right',
  pageIndex: editor.activePage,
  pageSize: editor.pageSizePoints(editor.activePage)!,
});
for (const s of await verifySignatures(editor, bytes!)) console.log(s.summary);

The Digital ID is RSA-2048 with a non-extractable private key kept in IndexedDB. Signatures prove the document was not modified after signing; they are self-signed, not a qualified electronic signature.

Browser support

The engine needs WebAssembly threads, WebGL2 and SharedArrayBuffer on an isolated page. The automated test suites run in current Chrome at desktop and phone sizes, and Safari is used in development; other browsers with those features should work but are not part of the test matrix yet. If your font picker offers the user's installed fonts through the browser's queryLocalFonts, note that only Chromium has it; elsewhere, let the user upload a TTF or OTF and pass its bytes to setFontData.

Troubleshooting

SymptomCause
"the page is not crossOriginIsolated"COOP/COEP missing, or the page is not HTTPS — check self.crossOriginIsolated in the console
PDFLicenseErrorno key, or the key does not list this domain — err.license.reason names the host it saw
404 for pdfeditor.js or pdfeditor.wasmwasmUrl does not point at the served wasm/ folder — or setLicenseKey ran before create without the same wasmUrl
A cross-origin image or font stops loading on the editor pageCOEP require-corp: the resource must send Cross-Origin-Resource-Policy or be same-origin

Every method: Web API reference.