Document features
PDFDocument and its namespaces.Example
// A bearer token (or any header the server wants) const doc = await PDFDocument.fromUrl('/api/documents/123', { headers: { Authorization: `Bearer ${token}` }, }); // The session cookie, on a cross-origin file host const doc2 = await PDFDocument.fromUrl('https://files.example.com/123.pdf', { credentials: 'include', }); // The same options on a progressive load: every range request carries them const doc3 = await PDFDocument.fromUrlProgressive('/big-report.pdf', { chunkSize: 256 * 1024, // range-request size (default 256 KiB) onProgress: (loaded, total) => updateProgressBar(loaded / total), headers: { Authorization: `Bearer ${token}` }, });
Certified documents: what each certification level forbids
| changesAllowed | Grayed out in the viewer | Refused in the document API |
|---|---|---|
'none' | Every editing tool, filling and the page actions | Every write below; doc.forms.setValues() rejects every field as read_only |
'forms' (filling and signing) | The comment tools (marks, notes, shapes, stamps, links, text boxes), the page-editing tools and the page actions | doc.annotations, doc.pages, doc.outline and doc.attachments writes |
'annotations' (those and comments) | The page-editing tools and the page actions | doc.pages, doc.outline and doc.attachments writes |
Notes
One sidebar, five tabs. Pages (thumbnails), Outline, Attachments, Signatures and Comments share a single panel (260 px; 320 px for Comments) with a rail of tabs beside it; one panel shows at a time. Collapsed, the rail stays, and a dot marks any tab that has something in it, so an outline or an attachment is never missed. The toolbar toggles open their tab (or collapse the sidebar when that tab is already showing); sidebar.pages.defaultOpen decides whether the Pages tab starts open. Below the compact breakpoint the sidebar is a drawer over the page. Empty tabs say what the feature is for and offer the action — "No outline yet · Bookmarks let readers jump to a section · Add a bookmark". viewer.openPanel() opens or closes a tab from your own chrome, activePanel says which is open, and panelChanged reports every change (sidebar.rail: false leaves the rail and the collapse button to that chrome). See Sidebar.
Outline (bookmarks) panel. The Outline tab renders the bookmark tree with chevrons to collapse a branch and the page number on the right; clicking an entry jumps to its page. Programmatic access: doc.outline.get(): Promise<OutlineNode[]> (every title '' under a license without the extract feature; the panel shows them either way).
Clickable document links. Links embedded in the PDF act when no tool is active: web links open in a new tab (scheme-allowlisted, so javascript:/data: URIs in hostile documents are never followed; opened with noopener,noreferrer), internal links jump to their page. A text mark whose quads no page could hold — a hostile file's — is clamped to the page as it loads, so drawing it can never exhaust the tab's memory; a page is read as at most 4 096 annotations, and a file whose page tree claims pages it does not hold is refused at open (FORMAT_ERROR), so no panel builds a card or a frame for what is not there. A link's uri as the API reports it is an address the viewer would follow (http:, https:, mailto:, tel:); a link with any other scheme reads as no link, so an href built from it is safe. Disable via links.enabled: false. Before a web link opens, a small dialog names the site and shows the full address (or the addressee of a mailto: / the number of a tel:) and waits for the reader to confirm — a document written by someone else can put any address under any words. Hovering a link shows a pointer cursor and a label (the link pill) with its destination. Skip the question with links.confirmExternal: false; drop the pill with links.pill: false, after which a tap on a touch screen follows the link at once (the confirmation above still names a web address). With callbacks.onExternalLink set, the SDK neither asks nor opens: the checked address goes to your handler instead, and links.confirmExternal no longer applies. See Links.
Bookmark authoring. In FULL mode, under a license with the edit-pages feature, and when the document's permissions allow it, the outline panel includes add / rename / delete controls (each edit goes to undo history and is included in exports). Rename and delete sit on the row (visible on touch, on hover with a mouse). For documents without an outline the empty state offers "Add a bookmark" to create the first one. Hide the editing UI with sidebar.outline.editing: false (the panel then becomes read-only and the outline toggle only appears for documents that have an outline). Programmatic API: doc.outline.add(title, pageIndex, { parentPath? }), doc.outline.delete(path), doc.outline.setTitle(path, title), doc.outline.set(nodes) — paths are index arrays into doc.outline.get(); each needs edit-pages.
Page rotation API. doc.pages.rotate(pageIndex, by) turns a page by clockwise degrees (90; -90 counter-clockwise, 180, 270), doc.pages.setRotation(pageIndex, 0 | 90 | 180 | 270) sets it outright and getRotation(pageIndex) reads it; a viewer on the document refreshes and takes the undo step. Turning a page changes the document, so rotate and setRotation need edit-pages.
Page text extraction. doc.text.extract(pageIndex): Promise<string> resolves to a page's plain text. Needs extract.
Attachments panel. The Attachments tab lists the document's embedded files with a file-type badge and size — enough to tell two invoices apart — and a download button on every row; clicking a name downloads too (filenames from the document are sanitized before use). The tab appears for documents that carry attachments, or always when editing is enabled; its empty state says what attachments are for.
Attachment authoring. In FULL mode, under a license with the edit-pages feature, and when the document's permissions allow it, "Attach a file" embeds a picked file into the document; each row carries remove beside download, visible without hovering. Every edit goes to undo history and is included in saved/exported bytes. Disable the editing UI with sidebar.attachments.editing: false, or the whole panel with sidebar.attachments: false. Programmatic API: doc.attachments.list(): Promise<AttachmentInfo[]>, doc.attachments.data(index): Promise<Uint8Array>, doc.attachments.add(name, bytes), doc.attachments.delete(index) (these two need edit-pages). Without the extract feature every name is '' and the bytes are refused; the panel still lists the files by name.
Comments panel. The Comments tab lists every comment in the document, page by page, as Acrobat's Comments pane does: each note, highlight, shape, stamp or other mark with who made it and when, what it says, its replies (replies to replies included, oldest first) and its review status. A card scrolls to its annotation and outlines it. Search covers what the comments and their replies say and who wrote them; filters narrow the list by author, kind and status. A document of up to 100 pages is read in the background as it opens — in quiet moments only, the pages on screen first, and on a document whose pages are slow to open only around them — so the tab's dot and count appear on their own; a longer one, and the rest of a heavy one, is read when the panel opens, viewer.showComment() is called or a commentsChanged or annotationsChanged listener is added (a listener reads every page once, promptly: the baseline its changes are told from). The cards exist only while the panel shows. The tab is offered under every license — reading comments is viewing — and read-only without the annotations feature, which replying, editing, deleting and setting a status need; turn it off with sidebar.comments: false.
On the page. A mark that carries a comment (a highlight, shape or stamp with words or replies; a note is its own icon) has a small round marker at its corner, as in Acrobat; a tap on it opens its card. A click on a commented mark opens its card too — with Select or Pan, or with the panel already open — while a click that ends a text selection or a pan does not. Comment on selected text: the selection bar's Comment highlights the text and opens its card with an empty editor, as Acrobat's does; discarding the empty comment removes the highlight again.
Replies and review status. In FULL mode, and when the document's permissions allow comments, every card has a reply box (Ctrl+Enter or ⌘+Enter sends) and a menu with Edit, Set Status (Accepted, Rejected, Cancelled, Completed, None) and Delete. Each reviewer has one status per comment, their latest; the card shows the latest anyone set (one reviewer clearing theirs leaves the others'), and every reviewer's on hover. The author is annotations.author. Every change is one undo step. Disable the editing UI with sidebar.comments.editing: false. A status is written as Acrobat writes one — a hidden note reply carrying the status, "Accepted set by …" as its words — and read from Acrobat's files the same way, checked on a document Acrobat itself reviewed; a chain of statuses as Foxit writes them is read to the comment at its top.
Who may change what, by Acrobat's rules. ReadOnly: nothing. Locked: it can be replied to and reviewed, not edited or deleted. LockedContents: all but editing its words. annotations.ownCommentsOnly: true applies Acrobat's shared-review rule — only a comment's author (its stored author equals annotations.author) edits or deletes it, anyone replies and sets a status; off by default, as on an ordinary file in Acrobat. viewer.commentPermissions() answers the same question for a host's own UI.
Written as Acrobat writes them. A reply is a note marked as answering the comment, over the comment's own rectangle, in its color and opacity, with a pop-up of its own and no subject; a status is a hidden reply carrying a review status. Every note, and every mark given words, gets the pop-up Acrobat gives it. So Acrobat lists them in the same thread. Replies are never drawn on the page: a reply is part of its comment's thread, so the viewer, printing and flattening leave out its note icon, as Acrobat does, and the Note and Erase tools never offer it. Deleting a comment — in the panel, with a tool or through the API — deletes its replies, statuses and pop-ups with it; moving, resizing or editing a mark keeps its thread.
Comments API. doc.annotations.comments(pageIndex?), get(id), reply(id, { contents, author }) and setStatus(id, status, { author }); REVIEW_STATUSES; viewer.showComment(), viewer.commentPermissions(), viewer.openPanel() and viewer.activePanel; the commentsChanged event says what changed on each page. See doc.annotations.
Signatures panel. For documents that declare digital signatures, the Signatures tab lists each one as the document declares it (signing time, reason, format, certification level). It leads with Not verified. — nothing here is checked against a certificate, and cryptographic validity is not verified by the SDK — so every signature carries a gray UNVERIFIED badge — or, when the document changed after it, an amber CHANGED SINCE one — and is "declared", never "signed by"; there is never a tick and never the word "valid". Amber is kept for the one thing the SDK can detect: a signature the document changed after (the bytes it signs stop short of the file as opened, or an edit was made in this session). A footer notes that changes are saved after the signed version: a signed document is always saved as one incremental update on the bytes it was signed over (see doc.save()), so a reader reports a change the signature's certification allows (a comment, a filled field) as made after signing, and only a change it forbids as breaking it. Hide with sidebar.signatures: false. Programmatic API: doc.signatures.list(): Promise<SignatureInfo[]> (each reason '' under a license without the extract feature; the panel shows it either way).
Certified documents are honored. A certification signature says what may change after it (SignatureInfo.changesAllowed, the certification level): 'none', 'forms' (filling and signing) or 'annotations' (those and comments). The SDK enforces it as Acrobat does. In the viewer, what the level forbids is grayed out with the reason ("This document is certified: only filling and signing are allowed"; a tool that edits the page itself — Add text, Edit text, Image, Sign, Erase — says "its pages cannot be edited", since no level allows that, whatever it allows of signing a field); the table above lists each level. A signature field is signed through the form at 'forms' and above. A certification signed through a field nested under a parent field, as iText writes them, is found through the document's own certification record and enforced the same. In the document API, a write the level forbids — doc.annotations (create, update, delete, reply, setStatus, an XFDF import with annotations), doc.pages (insert, delete, move, rotate, duplicate, import), doc.outline and doc.attachments — rejects with INVALID_STATE unless the call passes invalidateSignatures: true, the same escape as flattening or protecting a signed document: the certification is then broken for good and nothing is asked again; doc.forms.setValues() rejects every field as read_only at 'none'. What the level allows is written and saved as an incremental update, which Acrobat reports as "certified with changes", not as broken. A signed document with no certification (an approval signature) is not restricted: its changes are reported, as they always were.
Three things to know about certified documents: an export with invalidateSignatures writes a broken copy and leaves the open document certified and guarded; an undo after a write made with the escape restores the certified bytes but not the guard, which asks no more that session; and a license that marks exported pages adds a stamp to each, which a certification at 'none' or 'forms' forbids — a watermarked export of such a document reads as broken.
Loading from protected URLs. fromUrl and fromUrlProgressive hand headers, credentials and signal to fetch() as given, so a document behind authentication opens without the host downloading it first (see the example above). A same-origin URL sends the cookie by default (credentials: 'same-origin'). A cross-origin one needs credentials: 'include', and the server must then answer the page's exact origin and Access-Control-Allow-Credentials: true — a wildcard origin fails as CORS_ERROR, as with any cookie-backed fetch(). A 401 or 403 rejects with NETWORK_ERROR; its message ends in the status (HTTP 401). Refresh the token and call again. The headers are read once, at the call. A token that must be refreshed per request, or that expires during a long progressive load, is the host's to fetch: download the bytes with your own request and pass them to fromBytes. Pass signal to cancel a load the user navigated away from; it rejects with ABORTED.
Progressive loading. PDFDocument.fromUrlProgressive opens the document over HTTP range requests: the viewer appears and the first pages become viewable after a small fraction of the file has downloaded, while the rest streams in the background. Measured on a 107MB PDF: the document opened with about 0.2MB downloaded and the first page rendered after ~6MB — instead of waiting for all 107MB. Works best with linearized ("fast web view") PDFs but benefits any PDF. Requires a server that honors Range requests (checked automatically; falls back to a normal full download when it does not).
Until doc.isFullyLoaded is true the document is view-only: the editing tools are grayed out (toolBlockedReason() answers reason downloading, "The document is still downloading"; activateTool() rejects with INVALID_STATE), and undo snapshots, page management, search and export wait for completion (doc.whenFullyLoaded()), which the background fill reaches shortly after opening. Form fields can be filled meanwhile; the render worker starts then. The document API does not wait: doc.save(), doc.pages.extract() and every write through its namespaces reject with INVALID_STATE until then — a document saved before every byte has arrived would come back with empty pages. A download that cannot finish rejects whenFullyLoaded() with NETWORK_ERROR after a few retries. Pages scrolled to before their bytes arrive download on demand (doc.ensurePageAvailable(pageIndex) — the viewer does this automatically for pages and thumbnails).
Page boxes. doc.pages.boxes(pageIndex): Promise<PageBoxes> reads the page's Media/Crop/Trim/Bleed/Art boxes without loading the page. The viewer is CropBox-origin aware end to end: display layers (text layer, links, selection, search highlights, form fields) and all authoring tools (annotations, drawing, Add/Edit Text) align correctly on print-oriented documents whose CropBox origin is not (0,0).
Embedded thumbnails. Documents that embed thumbnail images (common in archival and print workflows) get instant thumbnail-sidebar paints from the embedded images instead of rasterizing every page. Documents without embedded thumbnails render exactly as before — the fast path is automatic and has no configuration.