# Document features

What the viewer does with the document itself: the sidebar and its five tabs, the outline, document links, attachments, comments, digital signatures and certified documents, loading from protected URLs, progressive loading, page boxes and embedded thumbnails. The options live in [Sidebar](https://kernstra.com/docs/web-sdk/config/sidebar), [Links](https://kernstra.com/docs/web-sdk/config/links) and [Annotations](https://kernstra.com/docs/web-sdk/config/annotations); the programmatic side in [`PDFDocument`](https://kernstra.com/docs/web-sdk/api/pdfdocument) and its namespaces.

## Example

```ts
// 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()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/openpanel) opens or closes a tab from your own chrome, `activePanel` says which is open, and [`panelChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-panelchanged) reports every change (`sidebar.rail: false` leaves the rail and the collapse button to that chrome). See [Sidebar](https://kernstra.com/docs/web-sdk/config/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[]>`](https://kernstra.com/docs/web-sdk/api/doc-outline/get) (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](https://kernstra.com/docs/web-sdk/config/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? })`](https://kernstra.com/docs/web-sdk/api/doc-outline/add), [`doc.outline.delete(path)`](https://kernstra.com/docs/web-sdk/api/doc-outline/delete), [`doc.outline.setTitle(path, title)`](https://kernstra.com/docs/web-sdk/api/doc-outline/settitle), [`doc.outline.set(nodes)`](https://kernstra.com/docs/web-sdk/api/doc-outline/set) — paths are index arrays into `doc.outline.get()`; each needs `edit-pages`.

**Page rotation API.** [`doc.pages.rotate(pageIndex, by)`](https://kernstra.com/docs/web-sdk/api/doc-pages/rotate) turns a page by clockwise degrees (`90`; `-90` counter-clockwise, `180`, `270`), [`doc.pages.setRotation(pageIndex, 0 | 90 | 180 | 270)`](https://kernstra.com/docs/web-sdk/api/doc-pages/setrotation) sets it outright and [`getRotation(pageIndex)`](https://kernstra.com/docs/web-sdk/api/doc-pages/getrotation) 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>`](https://kernstra.com/docs/web-sdk/api/doc-text/extract) 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[]>`](https://kernstra.com/docs/web-sdk/api/doc-attachments/list), [`doc.attachments.data(index): Promise<Uint8Array>`](https://kernstra.com/docs/web-sdk/api/doc-attachments/data), [`doc.attachments.add(name, bytes)`](https://kernstra.com/docs/web-sdk/api/doc-attachments/add), [`doc.attachments.delete(index)`](https://kernstra.com/docs/web-sdk/api/doc-attachments/delete) (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()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/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?)`](https://kernstra.com/docs/web-sdk/api/doc-annotations/comments), [`get(id)`](https://kernstra.com/docs/web-sdk/api/doc-annotations/get), [`reply(id, { contents, author })`](https://kernstra.com/docs/web-sdk/api/doc-annotations/reply) and [`setStatus(id, status, { author })`](https://kernstra.com/docs/web-sdk/api/doc-annotations/setstatus); `REVIEW_STATUSES`; [`viewer.showComment()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/showcomment), `viewer.commentPermissions()`, [`viewer.openPanel()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/openpanel) and [`viewer.activePanel`](https://kernstra.com/docs/web-sdk/api/pdfviewer/activepanel); the [`commentsChanged`](https://kernstra.com/docs/web-sdk/api/events#ev-commentschanged) event says what changed on each page. See [`doc.annotations`](https://kernstra.com/docs/web-sdk/api/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()`](https://kernstra.com/docs/web-sdk/api/pdfdocument/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[]>`](https://kernstra.com/docs/web-sdk/api/doc-signatures/list) (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`](https://kernstra.com/docs/web-sdk/api/pdfdocument/fromurl) and [`fromUrlProgressive`](https://kernstra.com/docs/web-sdk/api/pdfdocument/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`](https://kernstra.com/docs/web-sdk/api/pdfdocument/frombytes). Pass `signal` to cancel a load the user navigated away from; it rejects with `ABORTED`.

**Progressive loading.** [`PDFDocument.fromUrlProgressive`](https://kernstra.com/docs/web-sdk/api/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`](https://kernstra.com/docs/web-sdk/api/pdfdocument/isfullyloaded) is true the document is **view-only**: the editing tools are grayed out ([`toolBlockedReason()`](https://kernstra.com/docs/web-sdk/api/pdfviewer/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()`](https://kernstra.com/docs/web-sdk/api/pdfdocument/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)`](https://kernstra.com/docs/web-sdk/api/pdfdocument/ensurepageavailable) — the viewer does this automatically for pages and thumbnails).

**Page boxes.** [`doc.pages.boxes(pageIndex): Promise<PageBoxes>`](https://kernstra.com/docs/web-sdk/api/doc-pages/boxes) 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.


Web page: https://kernstra.com/docs/web-sdk/guides/document-features
