Release Next v3.0.0-next.14
- #827 by @bobsingor – Describe measurement annotation fields and add typed APIs to read page viewports and update page calibration.
-
#827 by @bobsingor – Expose page measurement calibration and viewport reads in the cloud engine, and deliver viewport change events with document version coherence.
-
#800 by @LazyCompiler – Cloud-backed search honours the
ignoreWhitespacequery flag, which travels inside the search token, and search cursors are pinned to it: replaying a cursor minted with the flag against a query without it (or vice versa) is rejected withInvalidArg, matching the local engine.
- #827 by @bobsingor – Add generated page viewport and scale APIs and measurement annotation types.
-
#827 by @bobsingor – Persist page measurement calibration in layer artifacts with authorization and audit events. Calibration advances the document version while retaining existing page cache versions and annotation scales.
-
#800 by @LazyCompiler – The layer search routes (
/v1/docs/:docId/layers/:layerName/search/{rects,full}/data) acceptignoreWhitespace=truealongside the other query flags and forward it to the engine, so a cloud search forinvoicefinds a letter-spacedi n v o i c e. Combining it withregex=trueis rejected withInvalidArg, and the flag is carried by the search tokens that page through results.
-
#827 by @bobsingor – Add distance drawing with separate endpoint and leader placement, four geometry handles, and directly draggable captions. Keep previews, hit testing, and selection bounds aligned, including short dimensions with outside arrows and displaced-label connectors. Measurement labels use the engine's PDF-coordinate rounding rules.
Rotate measurements around the center of their complete oriented selection frame, including leaders and displaced captions. Use the same frame for pointer rotation, quarter turns, reset, selection, and hit testing, keeping the center stable after a saved appearance is reloaded. Attach the rotation handle to the selection border without extra measurement-specific spacing.
Add perimeter and area creation through the existing polyline and polygon gestures, live labels, direct caption dragging, and complete selection bounds. Carry manual PDF-space caption centers through whole-shape transforms while vertex edits leave them fixed. Reject invalid area creation and vertex edits.
-
#827 by @bobsingor – Add measurement DTOs, page calibration contracts, and shared distance, perimeter, area, and number-format helpers. Caption positions use PDF coordinates and support partial updates and explicit resets.
Report invalid geometry for crossing, overlapping, or degenerate area boundaries while accepting either winding and an explicit closing vertex.
-
#800 by @LazyCompiler – Add the
ignoreWhitespaceflag toSearchQuery. A literal query folded with it drops whitespace on both sides instead of collapsing it, soinvoicefinds the letter-spacedi n v o i c ethat OCR'd scans and tracked-out headings produce, andtotal amountfindstotalamount. Hits still span the original text including the dropped whitespace, andwholeWordboundaries are checked on the original text. The flag is literal-only —validateSearchQueryrejects it together withregex(ignore-whitespace-with-regex) — and it round-trips through search tokens.foldTextgains the matchingdropWhitespaceoption, and the shared search conformance suite covers the flag.
- #827 by @bobsingor – Support distance, perimeter, and area measurements through the local engine. Add page measurement viewport reads and calibration writes, with persistence in saved PDFs and layers and viewport change events.
-
#827 by @bobsingor – Read and write PDF measurement dictionaries and viewports, derive labels on geometry or scale edits, and preserve imported labels on style edits. Move manual shape captions with rigid geometry transforms and validate measurement input before native writes.
Avoid an additional full-document buffer copy when exporting a saved PDF.
-
#800 by @LazyCompiler – Local engines honour the
ignoreWhitespacesearch flag: a query carrying it re-folds the cached page text with whitespace dropped, soinvoicefinds a letter-spacedi n v o i c e, and search cursors key on the flag so a resumed search never mixes hits from the two folds. Combining the flag withregexis rejected withInvalidArg.
- #827 by @bobsingor – Add the measurement feature entry with reactive calibration, page-scale, and readout hooks. Render rotated distance captions and use the standard square handles for measurement endpoints and leaders during annotation gestures.
-
#827 by @bobsingor – Add distance and calibration presets, point-based viewport scale selection, and per-annotation recalculation reports. Persist leader and caption edits in native PDF fields, preserve the measured endpoints during offset edits, and keep derived measurement values read-only in comments.
Use the standard selection spacing for measurement annotations.
Keep locally created and edited measurements vector-rendered after the engine saves their appearance, matching the existing annotation lifecycle and avoiding repeated switches to raster rendering.
Add area and perimeter presets with scale snapshots captured at the first vertex. Persist shape captions and derived values through ordinary annotation edits, retaining vector rendering after engine responses.
-
#827 by @bobsingor – Introduce page calibration, scale presets, units, precision, measurement readouts, and optional recalculation of existing annotations. Report partial results across pages and support session-only calibration on older engines.
Expose area unit choices and independent area-unit updates that preserve distance formatting.
-
#827 by @bobsingor – Add a Measure toolbar with distance drawing, a scale sidebar, and known-length calibration controls. Support Escape to cancel an unfinished drawing, with English and Spanish labels.
Use distinct calibration and scale-setting icons that inherit the viewer's theme and active-state colors.
Add Perimeter and Area tools, dynamic stroke-colored measurement icons across toolbar and cursor, separate area-unit controls, selected area/perimeter readouts, and a reset-label-position action.
- #827 by @bobsingor – Export MeasurementToken through every viewer entry point so applications can access page calibration and measurement controls through the viewer handle.
Release v2.15.1
-
#822 by @bobsingor – Fix text redaction when multiple regions intersect the same text object. Later regions no longer leave targeted text searchable or copyable in saved PDFs or remove neighboring text. Preserve the positions of remaining text, including vertical text, and leave text outside the redaction regions unchanged.
Remove stale
ActualText,Alt, andEreplacement text from affected marked content and structure ancestors, including unused inherited property resources. Keep unredacted uses of shared forms and properties intact, and correctly locate redactions inside transformed nested forms.Backports the critical redaction fixes for #801 to the v2 WASM build.
Release Next v3.0.0-next.13
-
#817 by @bobsingor – Expose structured FreeText rich text in OpenAPI annotation responses, including paragraph and run styles and custom font family names.
Update signature and version analysis response schemas with applicable restrictions, current-state findings, and information about later revisions and reverted objects.
-
#812 by @bobsingor – Add API contracts and OpenAPI schemas for preparing, completing, aborting, listing, and analyzing digital signatures. Add document version history, immutable version downloads, signed revision downloads, and signature contents and digest endpoints, with signing permissions and version conflict responses.
-
#812 by @bobsingor – Add
doc.signaturesfor cloud-backed signature inspection, revision analysis, and two-phase signing. Read signed bytes from immutable document versions and refresh the manifest after preparing or completing a signature.Support visual appearances on unsigned signature fields and publish their form update events.
-
#817 by @bobsingor – Add generated annotation response types for FreeText rich-text bodies, paragraphs, and styled runs, and allow custom font family names.
Expose restrictions, current-state findings, and later-revision details in the generated signature and version analysis response types.
-
#812 by @bobsingor – Add generated
doc.signaturesanddoc.versionsclients, request and response types, and package subpath exports. Support preparing, completing, and aborting signatures; reading signature facts and modification analysis; and downloading document versions, signed revisions, signature contents, and digests.
-
#812 by @bobsingor – Add durable two-phase digital signing with prepare, complete, abort, and expiry handling. Persist candidate data so completion can run on another replica, verify the supplied CMS, and reject completion when the document or layer has changed.
Publish each completed signature as an immutable document version while retaining the document ID and version history. Consume the signing layer's edits into the new base and refresh document state across replicas.
Add signature inspection and analysis routes, immutable version and revision downloads, and visual signature-field appearances. Use files and streaming storage transfers for signing candidates, with configurable temporary storage and signing expiry.
-
#817 by @bobsingor – Add pure helpers for normalizing rich-text runs, mapping text offsets, and reading or applying styles to a text range. Support rich-text updates and bold, italic, and underline properties in the annotation model while keeping plain contents synchronized.
Align FreeText and callout text insets, borders, and backgrounds with their PDF appearances.
-
#812 by @bobsingor – Introduce cryptographic signing and validation helpers for PDF digital signatures. Build, parse, and verify detached CMS signatures, validate certificate chains against application-provided trust anchors, and check a signer's response before completing a signature.
Provide WebCrypto and remote signer adapters, persistent personal signing identities, and signature verdicts that distinguish byte integrity, cryptographic validity, trust, and later document changes.
-
#812 by @bobsingor – Add digital signature and document version types, signing permissions, events, wire schemas, and the optional
DocumentSignaturesServiceAPI. Support signature-field authoring and appearances, file-backed layer inputs, file downloads, and configurable session and signed-document policies.Add revision change analysis and protection helpers that distinguish declared editing restrictions from the rules used to judge later modifications.
Prevent excessive processing time when decoding malformed base64 containing long runs of padding characters.
-
#817 by @bobsingor – Add rich-text document types and schemas for FreeText annotation reads, drafts, and patches, including paragraph settings and per-run style overrides. FreeText reads now include a rich-text representation even for plain text, and custom font keys are accepted in response schemas.
Add optional document font settings for embedding policy and typographic features, font identity and embedding-permission metadata, and explicit editing authorization for registered fonts.
-
#817 by @bobsingor – Support reading and authoring rich-text FreeText annotations through the local engine. Expose document-level font embedding and typographic settings through
doc.fonts.Return resolved font identities and embedding permissions from registration, and add
engine.fonts.authorizeEditing()for applications authorized to edit with preview-and-print fonts. -
#812 by @bobsingor – Add
doc.signaturesfor signature inspection, revision analysis, and two-phase signing with externally supplied CMS data. Support signature-field creation and visual appearances, expose the saved document version, and update the same document handle after signing.Open documents as immutable bases with editable layers by default, with
sessionKind: 'plain'available for unsigned documents. Preserve the loaded bytes on unchanged incremental downloads and add Node file-backed layer opens anddownloadToFile().Enforce declared signature restrictions by default, distinguish them from modification verdicts, and expose
signedDocumentPolicyfor applications that need to permit invalidating edits.
-
#817 by @bobsingor – Add native and WASM APIs to read FreeText rich text as JSON, author it from JSON or XHTML, and generate appearances with multiple fonts, run styles, and text decorations.
Support CFF font resources, document-level embedding and typographic policies, variable-font instancing, and font embedding-permission enforcement. Preserve registered font identities in saved annotation resources and expose identity and editing-authorization APIs.
-
#812 by @bobsingor – Add native and WASM APIs for signature inspection, revision comparison, byte-range digests, incremental signing, and signature-field appearances.
Make incremental layer saves omit unchanged objects and detect reverted edits, including after reopening a layer. Add file-backed layer and overlay reads, share immutable stream data, and compare stream contents in chunks to reduce copying and memory use.
Fix signature appearance placement, make newly authored form widgets printable, and resolve named pages through the current layer view.
-
#820 by @bobsingor – Fix native and WASM text redaction when multiple regions intersect the same text object. Later regions no longer leave targeted text searchable or copyable in saved PDFs or remove neighboring text. Preserve the positions of remaining text, including vertical text.
Remove stale replacement and alternate text associated with redacted content, correct redaction inside transformed nested forms, and preserve unredacted uses of shared images and forms.
Fixes #801.
-
#817 by @bobsingor – Read and write FreeText rich-text documents through the shared annotation services, preserve registered font keys on readback, and reject mismatched plain and rich text before applying a mutation. Default-style changes preserve explicit run overrides, while plain-text replacement resets run formatting.
Carry font identity, editing authorization, and document font settings through the worker protocol.
-
#812 by @bobsingor – Add signature readers, revision and working-copy analysis, two-phase signing, and session-independent candidate finalization. Support signature-field creation and visual appearances, enforce declared document and field restrictions, and install completed signatures as new immutable bases.
Preserve loaded bytes for unchanged or reverted edits. Use file-backed candidates, layer artifacts, overlays, and streamed downloads for native file sessions to reduce memory use, while retaining buffer-based support for WASM sessions.
-
#812 by @bobsingor – Add the
@embedpdf/react/signatureentry point with hooks for signing, signature snapshots, validation verdicts, protection, target fields, events, and saved signature libraries.Make signature widgets selectable for signing or inspection, and allow
useStampLibraries()to filter libraries by kind. Use the stamp capability contract in signature hooks to preserve plugin dependency boundaries.
- #817 by @bobsingor – Connect FreeText annotations to the shared rich-text editor so styled runs render and edit consistently, with text selections preserved during formatting. Paint annotation backgrounds only once to avoid doubling translucent fills.
-
#817 by @bobsingor – Add a framework-independent rich-text editor binding with styled paragraph rendering, DOM serialization, selection mapping, formatting commands, and plain-text paste handling.
Export
mountWebFont()to load font bytes for live editors and release shared font faces only after their last consumer unmounts.
-
#817 by @bobsingor – Add selection-aware rich-text editing for FreeText annotations. Font, size, color, bold, italic, and underline changes apply to selected text while editing, or to the annotation's default style otherwise.
Expose text-format commands and selection state, preserve styled runs through debounced writes, and map registered font families to the live editor's mounted fonts.
-
#812 by @bobsingor – Refresh widget appearances after visual signature fills and signing, and reload annotations when a new document version is published. Add box styling for signature widgets and expose annotation interaction priorities and the armed-stamp tool ID for cooperating plugins.
- #812 by @bobsingor – Add signature-field authoring and fill controls that expose whether a field is signed. Provide widget hit testing for placing marks on existing fields, and refresh form state after signing or a document version change.
-
#812 by @bobsingor – Introduce a headless signature plugin with digital signing, visual-only filling, and application-controlled confirmation modes. Place marks into signature fields or as free stamps, support certification and field locks, and configure external signers and trust anchors.
Expose signature snapshots, validation verdicts, document protection, signed revision downloads, and events for signing, inspection, and edits that would invalidate a signature. Reuse stamp libraries for signature and initials artwork.
- #812 by @bobsingor – Add persistent library kinds and filtering so stamps, signatures, and custom collections can share the library system. Support renaming and recategorizing libraries, authoring marks from drawn strokes, typed text, images, or PDF pages, and querying the currently armed asset for signature-field placement.
-
#817 by @bobsingor – Add bold, italic, and underline controls to the FreeText style panel while preserving the active text selection.
Support
annotations.fontsfor additional font choices. Fonts are fetched, registered with the engine, and mounted for the live editor before appearing in the picker, so editing and PDF output use the same font bytes. -
#812 by @bobsingor – Add signature and initials libraries with drawn, typed, and imported artwork, plus signature-field authoring, signing dialogs, and visual-only filling. Configure signers, trust anchors, certification options, and signing modes.
Add signature inspection with validation details, signed revision downloads, and notices when pending edits would invalidate a signature. Support filtering the stamps sidebar by library kind and showing quick-access stamps in the toolbar.
-
#817 by @bobsingor – Expose
AnnotationFontSpecandAnnotationsCustomizationthrough the viewer entry points so applications can configure additional FreeText fonts withannotations.fonts. -
#812 by @bobsingor – Expose
SignatureToken, signer adapters, personal key stores, and signature customization types through the viewer's core entry point so applications can configure and control signing from the viewer package.
Release Next v3.0.0-next.12
-
#803 by @bobsingor – The default stamp libraries are now self-describing, Acrobat-compatible PDFs. Each
<locale>/stamps.pdfcarries its name as/Title, registers every page in/Names /Pagesasidentifier=label(Approved=Goedgekeurd), and records the library id (embedpdf-standard), the locale, and each stamp's kind in/PieceInfo. Import one withimportLibraryPdffrom@embedpdf/plugin-stampand the title, identifiers, and labels come from the file; drop the same file into Acrobat's Stamps folder and it appears there. The artwork is unchanged from the previous release (every page renders pixel-identical). Locales: en, de, nl, fr, es, zh-CN, sv, ja. A new@embedpdf/default-stamps/libraryentry delivers each locale through the module graph:loadDefaultLibrary(locale)resolves to the library's bytes from a generated, lazily imported module, so the library ships as a chunk of your own build with nothing to copy and no CDN.LOCALESlists the shipped codes.The legacy
<locale>/manifest.jsonfiles remain available for existing v2 viewers that load this package from an unversioned CDN URL. They preserve v2's library and stamp ids, categories, labels, and page indexes, and point to the same adjacentstamps.pdffiles. V3 reads the PDF metadata directly and does not load these manifests.
-
#803 by @bobsingor – Add
doc.annotations.flattenanddoc.annotations.exportAppearanceto the public contract.Add
doc.pages.setNameanddoc.pages.removeNamefor registering, renaming, and removing named pages (/Names /Pagesentries) on a layer.
-
#803 by @bobsingor – The cloud engine implements
page(pon).annotations.flattenoverPOST …/annotations/pages/{pon}/items/flatten(content and annotation planes patched in place,annotations.flattenedpublished) andpage(pon).annotations.exportAppearanceoverPOST …/items/appearance(PDF bytes; a read).It also implements
pages.setNameandpages.removeNameoverPOST …/pages/namesandPOST …/pages/names/delete. Both advance onlydocVersionandlayoutVersion, so the cached manifest is patched in place, per-page render/text/annotation leaves stay valid, and the result carries the fresh layout includingnamedPages.
- #803 by @bobsingor – Add typed SDK methods for selective annotation flattening and appearance export through
client.doc.annotations.flattenandclient.doc.annotations.exportAppearance. Addclient.doc.pages.setNameandclient.doc.pages.removeNamefor registering, renaming, and removing named pages.
-
#803 by @bobsingor – Add
POST /v1/docs/{docId}/layers/{layerName}/annotations/pages/{pon}/items/flatten(flatten a chosen set of the page's annotations; gated like page flatten and persisted the same way — one page's content and annotation versions advance) andPOST …/items/appearance(the chosen annotations' appearances as one single-page PDF; gated bydoc.download, no-store).Add
POST /v1/docs/{docId}/layers/{layerName}/pages/namesandPOST …/pages/names/deleteto register, rename, or remove a/Names /Pagesentry on a layer. Both are page-structure mutations gated bydoc.pages.assemble: the worker writes a new layer artifact and the doc and layout versions advance, exactly like a page move, with no new resource, version, or cache scope./layoutresponses now includenamedPages.
- #803 by @bobsingor –
PluginContext.tryForDocument(token, documentId)—forDocumentfor an OPTIONAL dependency: null when the plugin is not installed or that document is not ready, never a throw (a workspace plugin reaching a sibling's document-scoped lens without requiring it).
-
#803 by @bobsingor – Selective annotation flatten and appearance export.
page(pon).annotations.flatten(refs, usage?)ispages.flattenfor a chosen set: painted annotations are removed from the page, ineligible ones (hidden for the usage, popups, no appearance) stay and reportskipped, and the result (AnnotationFlattenResult) carries that page's new pins plus anannotations.flattenedevent.page(pon).annotations.exportAppearance(refs)returns the chosen annotations' normal appearances as one single-page PDF sized to their union rect — vector, positions preserved, the source untouched; all-or-nothing. Both are optional service members;runAnnotationFlattenConformanceandrunAnnotationAppearanceExportConformancelock the shared behavior.Named pages join the page list.
PageListSnapshot.namedPagescarries the catalog's/Names /Pagesand/Names /Templatesregistrations in tree order —NamedPageEntryis the decoded key plus a target classified aspage(a page inpages),template(a hidden/Type /Templatepage that is never listed or rendered), ordangling. Because a registration only means something against the page set that contains its target, it is layout data likelabeland shares the layout version rather than a plane of its own.DocumentPagesServicegains two optional page-structure mutations:setName({ name, pageObjectNumber, replace? })registers a key, replaces what an existing key points at, or renames in one job;removeName({ name })drops a registration and keeps the page. Both return the fresh layout asPageNameResult(layout plus the docVersion/layoutVersion pins) and publish apages.namedevent. Deleting a page removes every registration pointing at it.runNamedPagesConformancelocks these invariants for every engine. The engine never interprets key text.Stamp
/Nameaccepts any non-empty name — a standard stamp name or a custom identifier such as an Acrobat library's#…key — and a stamp patch may clear it withname: null.
-
#803 by @bobsingor – The local engine implements
page(pon).annotations.flatten(gated likepages.flatten, publishesannotations.flattened) andpage(pon).annotations.exportAppearance(gated bydoc.downloadlikepages.extract).It also implements
pages.setNameandpages.removeName: register, rename, or remove a/Names /Pagesentry as a page-structure mutation gated bydoc.pages.assemble, with the fresh layout returned and apages.namedevent published.pages.list()includesnamedPages.The local engine never contacts a CDN. The zero-config default is the
embedpdf.wasmyour bundler emits beside your code (@embedpdf/engine-runtime-wasm32/wasm-url, which webpack, Vite, Rspack, Parcel, and Turbopack all resolve), and it is now streamed and compiled by the worker as it downloads — the previous fetch-then-fallback path had given up streaming. When a toolchain cannot carry that asset (Angular's application builder, plain esbuild), boot fails with the fix named instead of silently fetching a possibly mismatched binary from jsDelivr:DEFAULT_WASM_URLand the fetch-failure fallback are gone.New
@embedpdf/engine/portable: the samelocalEngine()with the wasm delivered through the module graph — a lazy chunk of your own build, gzipped and inflated in the browser — so it works with every bundler at the same cost over the wire, with no asset to copy.wasmLoaderjoinswasmUrl,assetsUrl, andwasmBinaryas an explicit source (bytes produced on demand at boot). Explicit sources never fall back.Angular needs neither: the package's export map routes the
es2020condition Angular's application builder resolves with to the portable build, so the plain@embedpdf/engineimport is zero-config under Angular too. Other bundlers do not declare that condition and keep the streamed asset.
-
#803 by @bobsingor –
EPDFPage_FlattenAnnotationsflattens a chosen set of a page's annotations with a per-entry status (applied, skipped, not-on-page), replacing the single-annotationEPDFAnnot_Flatten.EPDFPage_ExportAnnotationsAsDocumentflattens a set's appearances into a new single-page document sized to their union rect, replacingEPDFAnnot_ExportAppearanceAsDocumentandEPDFAnnot_ExportMultipleAppearancesAsDocument, which mishandled rotated appearances and the rect fit. Whole-page flatten, selective flatten, and export now share one candidate plan and one placement writer (ISO 32000-2 12.5.5 fit,/Matrixhonored, no content re-parsing); resources shared between exported appearances are cloned once.New named-page functions:
EPDFDoc_GetNamedPageCount,EPDFDoc_GetNamedPageAt(key as UTF-16 plus the value's object number and kind: page, template, or dangling),EPDFDoc_SetNamedPage(create or replace, pages in the page tree only),EPDFDoc_RemoveNamedPage, andEPDFDoc_RemoveNamedPagesForPage.EPDFDoc_DeletePageByObjectNumbernow removes the/Names /Pagesregistrations of the page it deletes. The name-tree index search reports a pair whose value is a missing object (with a null value) instead of hiding it and desynchronizing later indices.Annotation
/Nameis text:EPDFAnnot_SetNametakes any name, writes a name object (escaping applied by the serializer), and never touches/AP;EPDFAnnot_GetNamefills a text buffer. TheFPDF_ANNOT_NAMEenum, its subtype validation, and the sentinel that removed/Nametogether with/APare removed — remove/NamewithEPDFAnnot_RemoveKey(annot, "Name").
- #803 by @bobsingor – New
@embedpdf/engine-runtime-wasm32/wasm-inline:embedpdf.wasmas a gzipped, base64 ES module (about 3.6 MB on disk, the same bytes as the file over the wire), generated in the same build step as the binary. This is what@embedpdf/engine/portableimports lazily so the wasm can travel through the module graph where a bundler cannot emit it as an asset.
-
#803 by @bobsingor – New
annotations.flattenandannotations.exportAppearanceworker jobs over the runtime's set-based flatten: resolve refs on the page, hand the set to one candidate plan, and either paint in place (bumping that page's revision and weak-annotation state like a page flatten) or into a scratch document returned as PDF bytes.Every page list now includes the catalog's named-page registrations (
namedPages), read from the runtime with each value classified as page, template, or dangling. Newpages.setNameandpages.removeNameworker jobs register, rename, or remove/Names /Pagesentries and return the fresh layout; page deletion drops the registrations that pointed at the page.Annotation
/Nameis written and read as text: note and file-attachment icons map their ids to PDF names, stamps accept any non-empty name (standard or custom) and report custom names verbatim instead of collapsing them, and a stamp patch withname: nullremoves the entry without touching the appearance.
- #803 by @bobsingor – The stamp hover ghost is rendered at the on-screen device pixel size and re-requested when the zoom crosses a size bucket, so large vector stamps stay sharp.
@embedpdf/react/stampadditionally re-exportsindexedDbByteStoreandByteStorefrom@embedpdf/web, the browser store forpersistStampLibraries/restoreStampLibraries.
- #803 by @bobsingor –
indexedDbByteStore(dbName, { storeName? })is the browser's bytes-by-id store: one IndexedDB object store withlist,put, anddelete. It is the adapter for any plugin's DOM-free persistence port (structurallyStampLibraryStorefrom@embedpdf/plugin-stamp), written once here so every framework binding shares it.
-
#803 by @bobsingor – Stamps can be placed without the pointer and ghosted at any zoom.
placeStamp(input, placement)creates a stamp annotation by code — the same validation, fit, page clamp,/Name, and/Subja click afterarmStampproduces — and resolves to the new annotation's ref;StampPlacementnames the page, the anchor point, an optional width, and rotation.StampToolInputgainsname(the placed/Name) andsubject(the placed/Subj), and itspreviewnow also accepts aStampPreviewProvider: a function the hover ghost asks for a render at the device pixel width it is displayed at. Requests are bucketed to powers of two (previewBucket) and cached per bucket for the arm's lifetime, so a zoom gesture never renders per frame. The host capability'sarmedStampPreview(devicePixelWidth?)is now asynchronous and takes that width.armStampno longer clears the payload it just set when activating the built-in stamp tool from another active tool. Armed stamps now place correctly instead of falling through to the tool's source callback, while the legacyannotation-stamptool tag remains supported.
-
#803 by @bobsingor – The Insert tab's Stamp action now opens the stamps sidebar instead of a click-then-pick file dialog; the Image action handles arbitrary PNG and JPEG insertion. The sidebar is now the classic picker over real libraries. Its built-in library comes from
@embedpdf/default-stamps— the standard rubber stamps as one Acrobat-compatible PDF per locale — loaded on the panel's first open (never at boot), in the locale negotiated from the viewer's language and the browser's, and swapped when the viewer's locale changes; the canvas-drawn placeholder set is gone. The panel gets a library dropdown ("All stamps" plus one entry per library, shown once there are two), a two-column thumbnail grid with the label as tooltip, a hover×that removes a stamp from a user library, and per-library export as PDF and remove. Custom libraries persist in IndexedDB across reloads."Make stamp" joins the annotation selection strip: with one or more annotations selected on a page, it turns their appearances into a vector stamp in a "My stamps" library and opens the panel on it. Widgets and pending redaction marks are excluded.
New
stampsoption on the viewer customization:stamps: { defaultLibrary: false }ships no built-in library and makes no request (air-gapped);stamps: { defaultLibrary: 'https://your.cdn/{locale}/stamps.pdf' }self-hosts a copy of@embedpdf/default-stamps. The default is the copy that ships with the viewer, loaded as a lazy chunk of your own build; nothing is ever fetched from a third party. English and Spanish strings updated.
- #803 by @bobsingor – The
cloudpdf.jsartifact carries the built-in stamp library as lazy sibling chunks in its own folder instead of inlining eight locale PDFs as base64. Nothing is fetched from a third party; setstamps.defaultLibraryto self-host or disable it.
-
#803 by @bobsingor – The built-in stamp library ships inside the viewer instead of being fetched from a CDN. The npm entry keeps
@embedpdf/default-stamps/libraryexternal, so its locale modules become lazy chunks of your own build; the CDN snippet carries them as sibling chunks in its folder.@embedpdf/default-stampsis now a dependency. Previously a library build could inline the eight PDFs into the JS chunk as base64, a form webpack rejects.The viewer registers
stampPlugin(), exposesStampTokenthrough its drive door, and routes the Insert tab's Stamp action to the stamps sidebar. Arbitrary PNG and JPEG insertion remains available through the Insert tab's Image action.The CDN snippet (
dist/embedpdf.js) once again findsembedpdf.wasmwhen loaded from another origin: the wasm is now an asset Vite emits into the dist folder and references by a URL relative to whichever chunk needs it, instead of a path guessed against the entry file — which had resolved tochunks/embedpdf.wasmand failed. A cross-origin test (pnpm testin the viewer package, Playwright against the built artifact) now guards it: the snippet must render with every sibling fetched from its own folder and no request to any other origin.
Release Next v3.0.0-next.11
-
#793 by @bobsingor – Introduces the PDF action engine: one policy-gated dispatcher for the payload-carrying
/Aaction trees (execute/canExecute,dispatch/canDispatchby annotation ref). Domain plugins register executors and sinks through the/contract/hostlens (stage: goto/named; form: javascript/reset-form; annotation: the session-visibility sink), the framework installs a URI/Print UI adapter viasetUiAdapter, andonAction/onDiagnosticevent hooks report every dispatch. Chains walk in PDF/Nextorder with document-lifetime work first and navigation/external effects deferred until it succeeds;launch/goto-remote/media stay never-executable and incomplete trees are refused whole.Phase 2 adds annotation
/AAevents, ordered page lifecycle events, and the document-open sequence. Dispatch now submits synchronously into the serial queue, returns per-step trigger results, derives origin centrally, and never rejects. The lifecycle coordinator buffers initial page state behind the open barrier and caps programmatic cascades;/Ashadows/AA U. The shared hover pump coalesces Exit→Enter pairs, and configuration can gate triggers and the open sequence.The package now publishes bundle-safe
/contractand/contract/hostentries. They expose the public and sibling-host protocols over the same token without pulling in dispatcher or plugin wiring; the package root remains the explicit implementation opt-in.
-
#793 by @bobsingor – Document lifecycle events + the submit sink chain (Phase 4). The five catalog
/AAverbs (ISO 32000-2 Table 200 — WC/WS/DS/WP/DP) dispatch through the trigger spine ({scope:'document', event:'will-save'|…}), with the open-ordering law: catalogOpenActioncan never run AFTER a lifecycle verb. NEWrunDocumentVerb('save'|'print', operation)— before-event → operation → after-event as ONE serialized queue op (two saves can never interleave; a before-event failure never cancels the verb; an operation throw skips the after-event and rethrows) — andprepareClose()(the cooperative WC door; scripts never run in teardown). The Print door fires WP → adapter EXACTLY once → DP under one latch: a nested print request (a WillPrint script callingdoc.print()) is suppressed with areentrant-printdiagnostic, never a second dialog; an adapter throw skips DP. SubmitForm graduates from hard-blocked to the SINK CHAIN — embeddersetSubmitHandler(consent = installation; detached-promise contract:executedmeans handed to the embedder) → the document's home (doc.forms.submit, engine-asserted, awaited) → blocked withno-submit-sink; the form plugin registers the ONE dataset resolver (registerSubmitResolver), both action nodes and scriptdoc.submitForm()effects normalize to oneSubmitIntent, and policy gains a'submit-form'row (user-origin only by default). The catalog read is memoized per document with rejection eviction. ShipssubmitEntriesToUrlEncodedfor handlers. The plugin still never touches a network. -
#793 by @bobsingor – THE JavaScript switch lives here now:
actionsPlugin({ javascript })owns the per-document ScriptHost realm, registers the realjavascriptexecutor (page-scoped world prefetch → boot → run → owner-sink commits → surface, ALL inside the host transaction — the commit-inside-the-boundary law), and exposes the transaction port +surfaceScriptResultoncontract/host. Owner commit sinks replace direct engine writes (registerFormCommitSink/registerAnnotCommitSink— each plugin commits AND reconciles its own model, so the PDF and the pixels can never diverge). Trigger provenance reaches executors (ActionContext.event+eventOf), and — full ISO — the Hide action routes by OWNING plane: field names and widget object numbers become formsetDisplayeffects, plain annotations become document flag patches; the session-visibility sink (SessionEffectSink/registerSessionSink) is REMOVED.ActionUiAdaptergainsalert/gotoPage(+ print context) as the ONE UI port for every script effect, with thedoc.printauthority gate upstream andonScriptDiagnostic/onScriptErrorhooks. A deterministic per-dispatch JS node cap (maxScriptNodesPerDispatch) bounds/Nextchains.
- #793 by @bobsingor – The annotation and form response schemas now describe action nodes as a payload-carrying discriminated union exposed through reusable OpenAPI components. Recursive
/Nextelements intentionally remain open ({}) for Fern compatibility, so generated SDK types showunknown[]for nested chains without duplicating the action model for every response path.
- #793 by @bobsingor – Action trees parsed from the hosted engine now carry full interpreter payloads (the discriminated
PdfActionNodeunion) andDocumentActionsSnapshot.openDestination; the snapshot schema defaultsopenDestinationso responses from older deployments still parse. Cloud actions conformance now gates the payload matrix and the destination-form/OpenActionon the HTTP + native-runtime path.
- #793 by @bobsingor – Action trees now use shared generated types for their payload-carrying union (destinations, URIs, named actions, Hide targets, ResetForm state, file specs) instead of repeating the same models for every annotation and form response path. Recursive
/Nextchain elements remainunknown[]— a Fern limitation of the emitted recursive schema, not missing data.
- #793 by @bobsingor – Action reads served by
/v1/docs/:docId/actions@:token(and the layer-scoped variant) now include interpreter payloads on every executable node and the destination-form/OpenActionasopenDestination, via the shared engine-services reader. The wire representation of action nodes changed shape (payload-carrying discriminated union); deploy server and clients from the same prerelease train.
-
#793 by @bobsingor – Complete the Acrobat AF forms support library in the scripting prelude:
AFNumber_Keystroke,AFPercent_Keystroke,AFDate_Format/AFDate_Keystroke(Ex),AFTime_Format(Ex)/AFTime_Keystroke,AFSpecial_Format/AFSpecial_Keystroke,AFRange_Validate,AFMergeChange,AFMakeNumber, andAFExtractNums, with Acrobat-compatible rejection alerts, a lenient scand-style date parser, and newh/hh/tt12-hour tokens inutil.printd. Forms built with Acrobat's standard number/date/time/special formats (for example the Apryse demo form) now validate, format, and auto-calculate instead of failing on missing globals.javaScriptSourcesFromActionTreenow narrows on the payload-carrying action-node union and collects onlyjavascriptarms; rendition/JSremains represented but is deliberately not executed. A newui-effect-suppresseddiagnostic also makes permission-withheld script UI effects observable. -
#793 by @bobsingor –
doc.submitForm(...)emits a submit INTENT (Phase 4): both Acrobat forms — positional(cURL, bFDF, bEmpty, aFields)and the argument object (cURL/aFields/bEmpty/cSubmitAs/bGet) — become a{kind:'submitForm'}UI effect (include-mode field names;cSubmitAsbeatsbFDF; nothing in the VM ever touches a network — resolution and the sink chain live outside). Doc-typed events now carryevent.target = the Doc object(Acrobat's WillSave boilerplate doesevent.target.getField(...)). The v1-frozen posture constants are honest again:submitForm: 'sink-chain',catalogLifecycleActions: 'execute-on-verb', page/annotation eventsexecute-*— the actions plugin's policy is the live authority these document. -
#793 by @bobsingor – The ScriptHost and the annots plane.
createScriptHostowns the ONE realm per document (its own serialized transaction port; lazy name-tree boot delivered exactly once to the first transaction; resource faults poison and lazily rebuild the realm; per-run and caller-sliced budgets). The script world grows the curated annotation plane:this.getAnnots({nPage})/getAnnot(nPage, name)over a caller-prefetched, page-scoped input (null when nothing matches — Acrobat parity; unfetched pages are a NAMED compatibility deviation),Annotwrappers with a per-subtype validity matrix (ANNOT_WRITABLE_KEYS, drift-guarded against the kind registry; no dictionary access — the Acrobat model),setProps/getProps, and diff-derived canonicalannotEffectsbesideformEffects(declared cross-plane commit order: form first, then annot). Ships the standardcolorobject (constants +convert/equalwith the documented G/RGB/CMYK vectors), Acrobat{type, name}event overrides, and — cycle fix — theScriptSandbox/ScriptSandboxFactorystructural contract now lives HERE (core-js-sandboximplements and re-exports it).
-
#793 by @bobsingor – Session-visibility overlay:
Model.sessionHiddenwithsetSessionHidden/forgetSessionHidden/clearSessionHiddenmessages, composed through the neweffFlags/effBearerlenses so Hide actions and scripts flip presentation-only visibility (paint, hit-testing, selectability, handles) without ever touching the document/Fflags — and a session-SHOWN annotation is live, not painted-but-dead. The overlay survives page reloads and is forgotten only on true deletes. -
#793 by @bobsingor –
sessionHidden,effFlags/effBearer, and the session-visibility messages are REMOVED — visibility is document truth again (/Fflags, mutated through the normal write path), and every paint/hit/selection gate reads the annotation's own flags.
- #793 by @bobsingor – Adds
createEventHook(the one capability-event primitive: subscribe-onlyEventHook<T>face, snapshot fan-out, listener error isolation, inert after dispose) andcreateSerialQueue(promise-tail serialization) to the core toolkit.
-
#793 by @bobsingor – The SubmitForm intent and the document's-home submit contract (Phase 4). The
submit-formnode grows an ATOMIC optionalpayload(url+fields+ decodedSubmitFormFlags+charSet?; absent = older-runtime extraction, the node stays recognized-inert — partial states are unrepresentable).decodeSubmitFormFlagsis the ONE ISO Table-240 decode (excludederived from bit 1, never duplicated; bit 12 isExclFKey, bit 13 reserved,EmbedFormis bit 14; bit 9 SubmitPDF dominates format with GetMethod kept alive). NEW:doc.forms.submit?(request)— the optional delivery-to-the-document's-HOME capability (present only where a home exists; the local engine truthfully lacks it), gated by the NEW grant-minteddoc.forms.submitscope (no PDF permission bit exists for submission, sopdf.permissionsnever expands it);FormSubmissionRequest/ReceiptDTOs + wire schemas fix the record shape (dataset entries + declared intent as METADATA — the home derives WHO submitted from its own verified session, and never fetches the PDF's URL). Conformance pins the payload extraction (/UFprecedence, flag decode, both degrade shapes) in both flavors. -
#793 by @bobsingor –
PdfActionNodeis now a discriminated union carrying full interpreter payloads — agotowithout its destination or auriwithout its URI is unrepresentable. Executable arms:javascript(script),goto(destination),uri(+isMap),named,hide(targets +/H),reset-form(fields: PdfActionTargetRef[] | null— null means/Fieldswas absent and every field resets — plus the exclude flag), the file-spec arms, andrenditionwith its optional/JS. Unreadable payloads degrade that node tounknownwith the newpayload-droppedtree warning.DocumentActionsSnapshotgainsopenDestination(the destination-form/OpenAction, schema-defaulted and mutually exclusive withopenAction),ActionReadBudgetgains target-entry and payload-text limits, andPdfDestinationSchemamoved to a neutraldto/module (re-exported from its old home). The wire entry now also exposesPdfActionWireComponents, the stable named-schema registry used by API generators. This is a breaking source-level contract for code that constructed or narrowed action nodes, recorded as minor only because the3.0.0-nexttrain is prerelease.
-
#793 by @bobsingor – SubmitForm payload getters (Phase 4).
EPDFAction_GetNodeSubmitForm(has-fields + the raw ISO Table-240 flag word; returns true only when the REQUIRED/Fresolved to a URL — a<< /FS /URL >>file specification with/UFpreferred over/Fper 7.11.2, or a bare string/Faccepted as a producer-compat extension),EPDFAction_GetNodeSubmitFormURL, andEPDFAction_GetNodeSubmitFormCharSet(PDF 2.0/CharSet, extracted not encoded)./Fieldsentries ride the existing shared target storage (GetNodeTargetCount/TargetName/TargetObjectNumbernow also answer for submit-form nodes) and the aggregate payload/target budgets. An unresolvable required component withholds the WHOLE payload — the reader degrades the node; never a half payload. -
#793 by @bobsingor – Add action-payload getters to the fork's detached action model: unified Hide
/T/ ResetForm/Fieldstarget accessors (EPDFAction_GetNodeTargetCount/Name/ObjectNumber, with-1marking a target list that could not be fully represented — a partial list must never execute),EPDFAction_GetNodeHideFlag(/H, spec default hide),EPDFAction_GetNodeResetForm(/Fieldspresence +/Flagsexclude bit — absent and empty are different states),EPDFAction_GetNodeURIIsMap, andEPDFDoc_GetOpenActionDestfor the destination-form catalog/OpenAction. Payload capture stays inside the build-time snapshot (nothing lazy, nothing stateful), guarded by new cumulative per-model budgets for target entries and payload bytes plus a destination-array element cap.
-
#793 by @bobsingor – The action model reader extracts the SubmitForm payload atomically (feature-detecting the Phase-4 runtime getters: an older runtime payload yields the bare recognized-inert node — the pin-lag skew case, unit-pinned; a NEW runtime withholding an unresolvable
/Fdegrades the whole node tounknown+payload-dropped)./Fieldstargets and the URL/CharSet strings charge the existing aggregate budgets. -
#793 by @bobsingor – The action-model walker now materialises interpreter payloads (destinations, URIs +
/IsMap, named-action names, Hide targets +/H, ResetForm three-state fields + exclude, file specs) with reserve-before-allocate budgeting — payload lengths are charged against the aggregate read budget before any scratch buffer is allocated, and name-tree script names ride the same budget.readActionModeland the annotation/form field readers take the owning document pointer;readDestinationmoved to a sharedfeatures/destinations/home. The link target is now a pure projection of the payload-carrying activate tree (linkTargetFromActionTree) — the duplicate native root-read is gone, anincompletetree projectsunsupported, and a malformed/Ano longer silently falls back to/Dest.
- #793 by @bobsingor – NEW
@embedpdf/angular/actionsentry:injectActionsUiAdapter(handlers?)— Angular's spelling of React'suseActionsUiAdapter(theinject*law), installing the SHARED default policy from@embedpdf/webso the two bindings can never drift; re-exports the@embedpdf/plugin-actionscontract. The parity gate goes green (actionsported;anchoredconsciously added to PENDING).
- #793 by @bobsingor – Route the Stage adapter's interaction dependency through the bundle-safe plugin contract entry instead of the interaction implementation root.
-
#793 by @bobsingor – New
@embedpdf/react/actionsentry withuseActionsUiAdapter(browser-default URI open throughsanitizeExternalUri+ print dialog, overridable per handler), auseCapabilityEventhook for capability event subscriptions, and link-layer delegation: chain-bearing URI links drop the nativehreffast path so the dispatcher runs the whole chain (the'dispatched'outcome opens nothing itself — the adapter owns it).Widget fill controls become always-active
/AAevent surfaces, link anchors feed link hover events, and scripting-provider defaults use the dispatch origin when deciding whether to suppress lifecycle/boot UI effects.Route sibling feature dependencies through plugin contract/helper entries. Annotation selection hooks and anchor equality helpers are split into leaf modules so form and annotation-menu entries no longer import the full annotation feature implementation.
Render PDF list boxes as visible native scrolling controls in both form surfaces, with stable optimistic selection and wheel isolation so row hit-testing, selection, and scrolling stay synchronized while engine writes complete. Combo boxes retain their baked resting appearance and native popup behavior.
Keep keyboard focus indicators above baked PDF appearances so checkboxes and choice controls display the same clear blue focus ring as text fields and radio buttons.
-
#793 by @bobsingor –
useActionsUiAdapteris the ONE script UI port: it gainsalertandgotoPagedefaults with the origin×phase visibility matrix (lifecycle/boot alerts and non-user print suppressed unless the embedder passes handlers — which receive everything, context attached).useFormScriptingProviderandFormScriptingUiHandlersare DELETED.
-
#793 by @bobsingor –
useActionsUiAdapteris now glue over@embedpdf/web'screateDefaultActionsUiAdapter— behavior-identical (the corpus alert matrix pins it); the default policy lives in ONE place for every binding. -
#793 by @bobsingor – Widget activation is a WIDGET behavior, not a push-button behavior: clicking ANY form widget now dispatches its
/Athroughform.activateWidget— text, toggle, and choice widgets (both the annotation-backed renderers and the standalone fill layer) join the button path. This makes real-world "fake buttons" work: producers ship Reset/Next/Hide controls as READ-ONLY text fields carrying a widget/A(ISO puts the activate action on the annotation dictionary, any field type), which previously died in the viewer because only push buttons routed clicks to activation while the pointer feed's mouseUp was — correctly — shadowed by/Aprecedence. Disabled controls become pointer-transparent so their clicks reach the activation surface (a disabled form control suppresses click events entirely); toggles keep Acrobat's order (the value change first, THEN the/A); push buttons keep their gated door (disabledstill blocks activation there). Proven end-to-end by a real-DOM regression test (rendered FormLayer over a real engine — the gap every prioractivateWidget()-direct test masked) and a read-only fake-button fixture in the plugin e2e.
- #793 by @bobsingor –
createDefaultActionsUiAdapter— the actions UI adapter's DEFAULT policy (the origin×phase visibility matrix: hover/lifecycle prints suppressed, boot/lifecycle alerts suppressed, sanitizeExternalUri URI opens, browser fallbacks), hoisted here and written ONCE so every framework binding (react, angular) ships the same behavior instead of forking it. Structural twins keep this package plugin-free per the layering law;overridesaccepts a getter for late-bound handlers.
-
#793 by @bobsingor – Registers the action engine's session-visibility sink (
applySessionVisibilityon the host lens) when@embedpdf/plugin-actionsis present, and carries the full activate action tree + annotation ref onLinkNavItemso the nav layer can delegate chains to the dispatcher.The pointer-driven hover diff feeds annotation
/AAcursorEnter/cursorExit through the shared hover pump while reducer-side clears, widgets, links, and tree-less items stay inert.LinkNavItemalso carries hover-event presence flags for the link plane.Publish bundle-safe
/contractand/contract/hostentries over the same annotation token, plus the focused/authoringhelper entry./internalkeeps its implementation-helper meaning and no longer serves as the sibling bundle boundary. -
#793 by @bobsingor – The session-visibility overlay is RETIRED —
applySessionVisibilityis replaced bycommitScriptEffects, the annotation DOCUMENT-commit sink: script/Hide effects become engineannotations.updatepatches (colors crossing the Acrobat-array → engine boundary,/APregenerated engine-side), authority-enforced, stop-on-failure, with the owner reconciling its own model per touched page.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for command tokens, definitions, resolved-command equality, and capability types without command plugin wiring.
-
#793 by @bobsingor – Widget activation joins the action engine: with
@embedpdf/plugin-actionsinstalled,activateWidgetdelegates the full/Atree to the dispatcher (return type is nowWidgetActivationResult, discriminating the two worlds), so Hide/ResetForm push buttons work with scripting disabled. The host lens exposes the interim executors' doors:runActivationScript(one JS node as a widget transaction) andresetFormAction(three-state target resolution, one batch reset skipping non-resettable families, recalculate after). Executor-driven form mutations ride the same serial mutation queue as user commits — strictly actions-queue → form-queue, never the reverse.The widget DOM-event door dispatches widget
/AAtrees, including coalesced hover events. Activation now submits synchronously through the dispatcher, and script UI effects carry their dispatch origin; print requests without document permission are withheld with an observable diagnostic.Form scripting is fault-tolerant per event: Keystroke, Validate, Calculate, and Format exceptions degrade to diagnostics while explicit
event.rc = falseremains a rejection and resource-budget faults still fail the transaction. Keystroke actions now run Acrobat's typing and commit passes so standard AF validators and custom transforms see the expected event shape.Publish bundle-safe
/contractand/contract/hostentries over the same form token, plus a focused/scriptinghelper entry./internalremains an implementation visibility surface rather than the sibling bundle boundary. -
#793 by @bobsingor – The submit dataset resolver + the reset() symmetry fix (Phase 4).
resolveSubmitDataset(registered as THE actions submit resolver) does a FRESH engine read and applies the ISO Table-239/240 semantics through the new sharedfield-selectionmodule: include-mode NAMES select descendants by FQN dot-prefix (fixing the ResetForm exact-match conformance bug too — Tables 241/242 want subtrees), the NoExport veto is unconditional (diagnosed when explicitly listed), explicitly listed push-buttons/signatures and unsupported/Vshapes are DIAGNOSED (submit-entry-unsupported), never silently dropped, and IncludeNoValueFields yields name-only entries.resetFormActiongains an origin parameter threaded from the executor's ActionContext — a lifecycle/hover ResetForm can no longer launder its recalculation alerts into user origin — and the publicform.reset(key)now rides the SAME shared reset core (one effects batch → refresh → V/C/F recalculation), so a dependent calculated field can no longer go stale through the API door. -
#793 by @bobsingor – The K/V/C/F pipeline rides the shared realm:
FormScriptingControllerkeeps its ordering,event.rcsemantics, two-pass keystroke, overlay, and fault ladder UNCHANGED, but acquires the realm through a transaction port — the actions plugin's per-document host in viewers, or a self-owned standalone host (createFormScriptingHost/ the{ config }constructor arm) for stamp and tests. The snapshot moves INSIDE the transaction boundary (commit(ref, value)/activate(ref, action)/recalculate()— no snapshot parameters).formPlugin({ scripting })is DELETED (the switch isactionsPlugin({ javascript })); the interimrunActivationScriptexecutor and the form-side UI-effect provider (setUiEffectProvider/FormUiEffectProvider) are gone — every script surface flows through the actions port with origin/phase attached, and the newcommitScriptFormEffectssink commits script form effects with engine write + snapshot AND annotation-plane reconciliation in one place.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the translation token, locale vocabulary, and capability types without i18n plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for interaction and platform-feedback tokens, pointer/tool protocols, and capability types without either plugin implementation.
-
#793 by @bobsingor –
activate()accepts an optionalLinkActivateContext; when the actions plugin is installed and the item carries its/Atree, activation delegates to the dispatcher and returns the new{ outcome: 'dispatched', dispatch }arm — named verbs execute and mixed/Nextchains behind links finally run. Without the actions plugin the classic root-projection path is unchanged.Link
/AAhover presence flags now rideLinkNavItemfrom the standalone source so the navigation layer can deliver cursorEnter/cursorExit without waking the annotation behavior plane.Publish a bundle-safe
/contractentry so navigation consumers can depend on the link protocol without pulling in source/effect/plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the metadata token, state, and capability types without event, reducer, or plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the page-edit token and capability type without mutation or plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the redaction token, state, pending-item, and capability protocols without destructive-apply plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the render token, paint-plan vocabulary, settings, and capability types without raster or plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the search token, query validation, results, and capability types without scan, effect, or plugin wiring.
- #793 by @bobsingor – Publish bundle-safe
/contractand/contract/hostentries over the same selection token so sibling features can use selection protocols without gesture, reducer, or plugin wiring./internalretains its framework implementation helpers and makes no bundle-purity promise.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for the shell token, surface state, options, and capability types without reducer or plugin wiring.
-
#793 by @bobsingor – The main stage lens registers
gotoandnamedaction executors with the action engine when present: GoTo destinations reveal through the camera, NextPage/PrevPage/FirstPage/LastPage execute, unknown named verbs report inert.The stage lens also reports placed/current/visible page state and user-versus-programmatic motion causes to the action lifecycle coordinator.
Publish a bundle-safe
/contractentry and a focused/destinationhelper entry so camera consumers do not pull in stage reducer/effect/plugin wiring.
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for stamp-library tokens, assets, previews, configuration, and capability types without import, render, or plugin wiring.
- #793 by @bobsingor – Dynamic stamp evaluation constructs its OWN standalone realm per detached stamp-asset document (
StampScriptingOptions— the opt-in and script observers stay stamp-config; the viewer document's shared host is never involved).
- #793 by @bobsingor – Publish a bundle-safe
/contractentry for view-registry tokens, view state, and capability types without reducer or plugin wiring.
- #793 by @bobsingor – The
ScriptSandbox/ScriptSandboxFactorystructural contract moved to@embedpdf/core-acrojs(cycle fix); this package implements and re-exports it, and threads the new annots plane +annotEffectsthrough the QuickJS bridge unchanged.
-
#793 by @bobsingor – The viewer wires the action engine in:
actionsPlugin()joins the plugin set and the Shell installs the default URI/Print UI adapter. -
#793 by @bobsingor – The chrome save/print commands become the Phase-4 verb owners:
document:downloadruns WS → serialize → DS as ONE queued operation (the WillSave mutations are IN the downloaded bytes; two rapid saves can never interleave) anddocument:printruns WP →window.print()→ DP under the reentrancy latch — both degrading to today's behavior when the actions plugin is absent. -
#793 by @bobsingor – Scripting migrates to
actionsPlugin({ javascript: { enabled: true } })with a bareformPlugin(); the Shell's form scripting provider hook is gone —useActionsUiAdaptercarries everything.
Release Next v3.0.0-next.10
- #791 by @bobsingor – Add
doc.annotations.listAllto the public contract for retrieving every page's annotations as one coherent snapshot with audit-head metadata.
- #788 by @bobsingor – Add coherent whole-document annotation snapshots using annotation versions and audit heads, including stale-version retries and desync-safe hydration. Expose per-record annotation creation, mutation, and group-assignment permission checks through the cloud document security service.
- #791 by @bobsingor – Add
client.doc.annotations.listAll()for retrieving every page's annotations as one coherent layer snapshot.
- #788 by @bobsingor – Persist per-layer annotation versions and add versioned page and whole-document annotation reads with audit-head metadata. Expose a no-store backend endpoint for listing every annotation in a layer. Annotation mutations now advance the version atomically so clients can hydrate and retry against coherent snapshots.
- #788 by @bobsingor – Add comment-thread and review-state composition, attached-link substrate helpers, conversation-plane filtering, and per-record annotation authority. Align annotation grouping, hit testing, geometry, and sparse property updates with the new comments, links, and permission model.
- #788 by @bobsingor – Clarify the public page transform API around content-space coordinates. Add
toPixelsandfromPixels, and rename the display conversions tocontentToView,contentToViewRect,viewToContent, andviewToContentRect.
- #788 by @bobsingor – Export
PermissionDeniedand addDocumentsCapability.allows()so hosts can gate document-level print and download controls using the active document's security policy.
- #788 by @bobsingor – Add annotation subjects, text review states, comment-thread composition, and coherent whole-document annotation snapshots. Extend document security with per-record collaboration checks, add versioned annotation wire resources and the public whole-document annotation path, and surface unrecoverable event gaps through
stream.desynced.
- #788 by @bobsingor – Implement local per-record collaboration permission checks for annotation creation, mutation, and group assignment. Update scope guards and the local runtime integration to support the new annotation and security contracts.
- #788 by @bobsingor – Read and write annotation subjects and text review states, preserve current annotation state during sparse writes, and use
EPDFAnnot_SetRectfor moves that must not regenerate appearances. Event delivery now reports gaps that require a full client refresh.
- #788 by @bobsingor – Rename
EpdfPageContext.toPagePoint()totoContentPoint()and align the Angular page context with the content-space page transform API.
- #788 by @bobsingor – Add comment-thread APIs with live page metadata and navigation rectangles. Align React page contexts with the content-space transform API, improve live free-text and callout rendering, and route link activation through the shared link-opening behavior.
- #788 by @bobsingor – Add whole-document annotation hydration with event replay and desync recovery, a comments and review-state API, and an attached-link lens backed by substrate annotations. Add per-record permission checks and keep conversation records out of painting, hit testing, and selection.
- #788 by @bobsingor – Add
canRead(),canFill(), andcanDesign()permission helpers, replacingcanModify(). Form hydration and mutations now refuse unauthorized work locally, and realtime desync events trigger a full form snapshot refresh.
- #788 by @bobsingor – Add
canMark()for redaction annotation creation and makecanApply()mirror every capability required by destructive redaction. Unauthorized selection queueing is now rejected before creating marks.
- #788 by @bobsingor – Add
canRender()and reject unauthorized page and tile renders locally withPermissionDenied, avoiding repeated engine requests for sessions without render access.
- #788 by @bobsingor – Add
canSearch(mode)so hosts can gate search and snippet controls using the session's text-search and text-copy permissions.
- #788 by @bobsingor – Add a comments and review sidebar with loading, retry, and navigation states, plus an anchored editor for annotation links. Commands and controls now reflect document and per-record permissions, with updated locale strings for the new states.
- #788 by @bobsingor – Expose the native
EPDFAnnot_SetRectbinding and update the packaged runtime build manifests used for rect-preserving annotation moves.
- #788 by @bobsingor – Mark grouped annotation link children as attached navigation items so link activation can defer to annotation editing instead of opening the target.
- #788 by @bobsingor – Allow
RevealOptions.rectto benulland align stage coordinate conversion with the content-space page transform API.
Release Next v3.0.0-next.9
- #772 by @bobsingor – Add API definitions for inserting pages from a PDF, inserting blank pages, and extracting selected pages as a standalone PDF.
- #772 by @bobsingor – Implement page insertion, blank-page creation, and page extraction in the cloud document pages service. Insertions invalidate stale manifest rows and publish local or realtime
pages.insertedevents with the updated layout.
- #772 by @bobsingor – Add generated SDK methods and request and response types for page insertion, blank-page creation, and page extraction.
- #772 by @bobsingor – Add document routes for inserting PDF pages, creating blank pages, and extracting selected pages. Insert mutations persist the new page set and versions atomically and publish audit events for realtime collaborators.
-
#787 by @bobsingor – Reusing an explicit
docIdondocuments.initordocuments.importnow answers a clean409 Conflictexplaining how to proceed (retry with the sameidempotencyKeyto resume, delete the document to replace it, or use a different/omitteddocId) instead of surfacing the raw database unique-constraint error as a 500 — including the case where a retry mints a fresh idempotency key per attempt. AdocIdalready owned by another tenant answers403, mirroring the existing delete semantics. -
#785 by @bobsingor – Fix AWS S3-backed streaming uploads by buffering small source chunks before the SDK applies checksum framing, preventing
InvalidChunkSizeErrorduring document imports and other streamed writes. Handled 5xx responses now also emit structured error logs with the request ID and HTTP status.
- #772 by @bobsingor – Export
PageInsertResult,PageInsertBlankSpec, andPdfSizefrom the main package so applications and plugins can type page-creation operations.
- #772 by @bobsingor – Add the required
pages.insertBlank(spec, destIndex?)document operation and makepages.insertandpages.extractrequired across engine implementations. Add the blank-page input types, wire protocol, HTTP paths and schemas, conformance coverage, andpages.insertedevent assertions.
- #772 by @bobsingor – Implement
pages.insertBlankin the local document pages service. Blank-page requests use the worker protocol, enforce the page-assembly capability, and publish the resultingpages.inserteddocument event.
- #772 by @bobsingor – Implement blank-page insertion with PDFium and dispatch the new
pages.insertBlankworker request. The implementation validates page size, count, and destination index, creates persistent blank pages, and returns their new page object numbers and layout.
- #772 by @bobsingor – Add
addBlank()andinsert()to the page-edit capability. New pages can be placed by page-object-number anchors or absolute index, and blank pages default to the size of the neighboring page.
- #772 by @bobsingor – Update canonical stamp-library import and append flows for the required
pages.insertandpages.extractengine operations, removing obsolete optional-feature checks.
Release Next v3.0.0-next.8
- #783 by @bobsingor – Use the document-scoped access endpoint for unlock requests. Add an opt-in
docAffinityHeaderoption for routing document requests and bounded retries for serverEngineBusyandEngineRestartingresponses, includingRetry-Afterhandling and anonRetrycallback.
-
#783 by @bobsingor – Add a production Helm chart with validated SQLite and Postgres profiles, safety gates, smoke and crash drills, and OCI publishing tied to the server package version. Add Prometheus metrics, drain-aware bounded shutdown and readiness, serialized Postgres migrations, and fail-fast worker supervision.
Add opt-in supervised engine-host process isolation with generation-fenced recovery, crash journaling, document quarantine enforcement, engine health reporting, and audited quarantine CLI commands. Repeated engine crashers can be observed or rejected with
DocumentQuarantined, while native host crashes restart the engine without terminating the API server.Encode page renders, annotation appearances, and warm thumbnails inside engine workers by default so compressed images cross the engine boundary, with a temporary API-side encoding fallback. Add bounded interactive and background scheduling, per-host memory telemetry, controlled engine recycling, and deterministic engine sharding with per-shard readiness and metrics.
Add
POST /v1/docs/:docId/accessas the document-scoped access endpoint while retainingPOST /v1/accessas a transitional alias. Head responses advertise the scoped endpoint, and path/body document ID mismatches are rejected. Allow the optionalX-CloudPDF-Docaffinity header through CORS and expose retry and image metadata response headers to browser clients.
-
#783 by @bobsingor – Add the
*.renderEncodedwire kinds (pages.renderEncoded,document.renderPageFileEncoded,annotations.renderAppearancesEncoded) plus theirRenderEncode/EncodedImageWireshapes — cloud-server surface (types only): the raster is encoded where it is produced and only the compressed image crosses the engine boundary.Make document access endpoints document-scoped by changing
wirePaths.accessto awirePaths.access(docId)builder. KeepwirePaths.accessLegacyfor transitional clients and allow the scoped endpoint to omitdocIdfrom the request body.
- #783 by @bobsingor – Add a public
@embedpdf/engine-runtime/build-idsubpath exposing the runtime's build identity (ENGINE_RUNTIME_VERSION,engineRuntimeTarget(), andengineRuntimeBuildId()) as a side-effect-free Node module. Supervisors and diagnostics can identify the version and resolved native target without loading the native addon.
- #783 by @bobsingor –
WorkerHostaccepts an optional injectedWorkerImageEncoder(third constructor argument) and dispatches the new*.renderEncodedkinds through it on a narrowly-scoped async path. No new dependencies: the native encoder stays in the injecting package. Hosts without an encoder (browser/local workers) reject those kinds withNotImplemented; existing two-argument construction is unchanged.
- #779 by @bobsingor – Bind Stage surface measurement before browser paint so the initial viewport and camera placement settle before page surfaces become visible. React viewers no longer show a transient incorrectly positioned page while a document opens.
- #779 by @bobsingor – Keep page and scrollbar screen geometry hidden until initial viewport placement commits. Stage consumers no longer receive origin-based placeholder geometry while viewport, responsive settings, and camera state are being initialized, preventing pages from rendering at the top-left before their final placement.
Release Next v3.0.0-next.7
- #776 by @bobsingor – Add
textQuadEdgeandtextQuadEqualshelpers for orientation-aware glyph edges and corner-wise text-quad change detection.
- #775 by @bobsingor – Add optional hints to capability tokens so missing-dependency errors can tell integrators which plugin to register.
- #775 by @bobsingor – Move Angular Stage input handling to the shared web surface controller, with lens-scoped interaction and native touch pan, pinch, fling, double-tap, and long-press gestures. Interaction now defaults on when the hub is present and can be disabled for secondary lenses.
- #775 by @bobsingor – Move React Stage input handling to the shared web surface controller, key page surfaces by durable page identity, and add draggable touch selection handles. Interaction now defaults on when the hub is present and can be disabled for secondary lenses.
-
#776 by @bobsingor – Refactor
SelectionHandlesto use the shared selection and web primitives so handles align correctly with rotated text and rotated pages. -
#777 by @bobsingor – Give every Stage lens and standalone
PageViewa stable view identity and use its scoped tile handle. Thumbnail and secondary views can no longer clear the main view's high-resolution tiles.
-
#775 by @bobsingor – Add shared browser Stage surface and touch gesture controllers with lens-scoped input, pan, pinch, fling, double-tap, long-press, and wheel handling. Export vibration and native-shell feedback providers.
-
#776 by @bobsingor – Add a shared native DOM binding for selection-handle drags that shields Stage gestures and handles pointer capture and client-delta tracking.
- #775 by @bobsingor – Add touch-aware tool consent, hit targets, drag handling, and cancellation so annotation editing cooperates with navigation gestures. Text-edit operations now report whether they handled a gesture, and the annotation capability token includes a missing-plugin hint.
- #775 by @bobsingor – Add pointer modality, cancellation, long-press, source metadata, touch-claim preflight, and source-scoped handlers to interaction routing. Introduce the PlatformFeedback capability and feedback plugin, and add a missing-plugin hint to the interaction capability token.
- #777 by @bobsingor – Scope tile state by view and page so multiple views of the same page can plan rasters independently without invalidating each other. Replace the flat tile methods with a reference-stable
render.tilesFor(view)handle that binds the view identity for planning, paint reporting, and release.
- #776 by @bobsingor – Add framework-independent selection-handle geometry and drag policies that follow rotated text, rotated pages, and RTL selection boundaries.
- #775 by @bobsingor – Route touch long-press selection through explicit gesture metadata, report whether word and line selection succeeded, and support optional selection feedback without swallowing unhandled gestures.
- #775 by @bobsingor – Add responsive container-query settings and named active rules. Add gesture lifecycle, elastic overscroll, fling, anchored double-tap zoom, and lens identity APIs, while removing the deprecated interaction settings and moving wheel classification to
@embedpdf/web.
- #777 by @bobsingor – Double-tap from a pinched-in zoom returns to fit-width instead of climbing (the iOS rule). The zoom ladder previously picked "the first posture meaningfully above the current zoom," so a pinch to a level between fit-width and detail made a double-tap zoom IN further. The rule is now: the ladder ascends only from ON a rung (within ±10% of a posture) — a tap at a posture moves to the next, wrapping past the top — while a pinch to any other level is leaving the ladder, and a double-tap there RESETS to the base fit ("take me back to reading"), never a further zoom-in. Everything that already felt right is unchanged: zoomed-out → fit-width, fit-width → detail, detail → fit-width.
- #775 by @bobsingor – Enable draggable touch selection handles and register vibration feedback by default when the platform supports it.
- #771 by @bobsingor – Accepts bodyless requests that carry
Content-Type: application/jsoninstead of failing them with an unhandled 500.- An empty JSON body now parses as no body rather than
FST_ERR_CTP_EMPTY_JSON_BODY— the shape the generated PHP, Go, and Ruby SDKs (and clients with default JSON headers) send for bodyless calls such as document delete. Non-empty bodies still go through Fastify's default parser, keeping its prototype-poisoning protection, and routes that require a body still reject its absence with a 400. - The error handler now honors Fastify's
statusCodeon framework errors, so parser and payload rejections (empty or malformed JSON, body limits) surface as the 4xx client errors they are instead of being logged and returned as "unhandled error" 500s.
- An empty JSON body now parses as no body rather than
- #775 by @bobsingor – Update the generated default WASM URL to use the current engine runtime release instead of the previous prerelease.
Release Next v3.0.0-next.6
-
#766 by @bobsingor – Extend
documents.importFromwithmode: "async". Async requests accept operator-registered connection sources and return 202 withtag: "accepted"and a pending document that callers can poll withGET /documents/:id; presigned URL sources remain synchronous. -
#766 by @bobsingor – Add the provider-neutral
connectionsource todocuments.importFrom. Requests identify an operator-registered connection and object key, plus an optional opaque provider-specific revision, without exposing storage-provider configuration or credentials in the public wire contract. -
#766 by @bobsingor – Add the
documents.importFromoperation for importing a PDF from a caller-supplied URL. The request supports optional size and SHA-256 integrity pins, metadata, deduplication, and idempotency fields, while responses distinguish completed imports from validation, authorization, conflict, and upstream transport failures. -
#766 by @bobsingor – Rename the contract operation from
documents.importtodocuments.importFromso generated Java, Python, and Ruby SDKs expose a consistent method name. The HTTP path remainsPOST /v1/tenants/{tenantId}/documents/import. The OpenAPI emitter now rejects group or method segments that collide with reserved words in those target languages.
- #766 by @bobsingor – Add the generated
documents.importFromclient method and request and response types. The SDK accepts URL or operator-registered connection sources, supports synchronous and asynchronous import modes, exposes integrity, deduplication, metadata, and idempotency options, and maps upstream transport failures toBadGatewayError.
-
#766 by @bobsingor – Add durable asynchronous document imports backed by the
document_importsjob table and an in-process worker with one claim loop per replica. Document and job creation is atomic, lease-token-fenced transitions prevent stale workers from overwriting replacements, reconcile-on-claim avoids duplicate transfers after crashes, and exhausted retries fail the document and clean destination bytes. Retries stay pinned to one content identity, filesystem sources requireexpected.sha256, and queued or running imports are protected from the stale-pending sweeper. Migration 027 stores the re-drivable source descriptor insource_json. -
#766 by @bobsingor – Add operator-registered import connections through
CLOUDPDF_IMPORT_CONNECTIONSfor S3 and S3-compatible stores, GCS, Azure Blob, and filesystem roots. Connections enforce credential classes, tenant allowlists, scoped prefixes or tenant-bound key templates, provider-specific revision pinning, and fail-closed authorization. Canonical backend fingerprints reject self-imports, and each import records sanitized source provenance and outcome in the newdocument_importstable from migration 026. A shared conformance suite keeps URL and connection adapters aligned on source-opening behavior. -
#766 by @bobsingor – Implement server-side PDF imports from caller-supplied URLs through the existing document lifecycle. Deployment policy controls size, timeout, concurrency, HTTPS, and public-network requirements; URL handling blocks private and metadata addresses with DNS pinning, rejects redirects, and requires
Content-Length. Imports enforce optional size and SHA-256 pins, sanitize failures so URL secrets do not leak, leave documents pending after retryable transport failures, and add thepullupload kind in migration 025.
-
#766 by @bobsingor – Mount the document import handler under the renamed
documents.importFromcontract operation and align the server's import policy and lifecycle terminology. The HTTP route and runtime behavior remain unchanged. -
#766 by @bobsingor – Cloud ObjectStore adapters (S3, GCS, Azure Blob) now truly stream
putbodies: aReadableis hashed and length-enforced as it flows (constant memory) instead of being buffered whole. Under- or over-delivery aborts before a visible object can appear, any prior object at the key survives a failed attempt, and the SHA-256 metadata is attached post-stream (S3 via a same-key server-side copy). FsObjectStore now cleans up its.partialfile when the source stream errors mid-put.
-
#768 by @bobsingor – Consolidate base-page and deep-zoom tile painting into
RenderLayer, with atilesoption for lenses that explicitly disable tiling. The separateTileLayersurface is removed because tile engagement is now render-policy arithmetic owned byRenderLayer.Tiles are positioned directly in view-pixel space and use the shared painted-image lifecycle, keeping retained coverage until replacements have a presentation opportunity and avoiding deep-zoom rounding drift, incomplete-image outlines, and transient seams.
- #768 by @bobsingor – Add
bindPaintedImage, a framework-neutral browser adapter for binding object-URL raster sources to image elements. It hides incomplete images, owns abort and URL-revocation cleanup, and reports painted and unpainted state around the image's presented lifetime so React, Vue, Svelte, and Angular adapters can share the same minimal lifecycle.
-
#768 by @bobsingor – Add a configurable render strategy for exact and lattice-backed deployments, with separate full-page and tile-plane budgets, format conformance, settled level selection, and public paint settings.
Deep-zoom tiling now uses bled overlap, presentation-aware generation retention, bounded fetch backpressure and raster residency, stage-less demand limits, stronger raster identities, failure isolation, and optional diagnostics. These changes keep tile memory bounded while preventing stale reuse, visible seams, and quality regressions during zoom and pan transitions.
- #768 by @bobsingor – Expose transient
cameraRestingstate and defer page-origin device snapping while zoom is moving. Pages retain fractional placement through continuous zoom and snap once the camera settles, preventing anchor jitter and per-step content movement without sacrificing crisp resting placement.
- #768 by @bobsingor – Fix the default inline image-encoder worker path so it creates the bundled blob worker instead of attempting to fetch
/inlineand silently falling back to main-thread encoding. Tile rendering now keeps encoding work off the main thread under the default configuration.
- #768 by @bobsingor – Bound individual annotation-appearance raster allocations at deep zoom by reducing the effective appearance scale while preserving the original placement rectangle. Oversized page-spanning appearances now degrade softly instead of exhausting the wasm heap with multi-gigabyte bitmap requests.
- #768 by @bobsingor – Adopt the unified
RenderLayerpage composition so the full viewer gets policy-driven deep-zoom tiling without mounting a separate tile layer. Base and sharp tile pixels now follow one rendering lifecycle through zoom, pan, annotation, and page-view surfaces.