PDF Generation Template Editor
Open-source visual PDF generation engine with customizable templates and developer-friendly APIs.
Open-source visual PDF generation engine with customizable templates and developer-friendly APIs.
ComPDFKit Web SDK follows a strict three-layer architecture. Understanding the boundaries between these layers is essential for customizing the UI and extending the SDK.
Integrator's page
└─ webviewer.js (iframe embed — packages/core/webviewer.js)
└─ Webview Vue app (packages/webview — OPEN SOURCE)
└─ Core bridge (packages/webview/src/core/)
└─ ComPDFKitViewer (packages/core — CLOSED SOURCE)
└─ Web Worker (compdfkit_worker.js)
└─ WASM (ComPDFKit.wasm / pdfium.wasm)| Layer | Location | State | Open source? |
|---|---|---|---|
| Embed entry | packages/core/webviewer.js | Creates the iframe | Yes (thin) |
| UI layer | packages/webview | Pinia stores | Yes |
| Core bridge | packages/webview/src/core/ | Stateless (one registry) | Yes |
| Engine | packages/core | WASM-backed | No |
webviewer.js The integrator calls ComPDFKitViewer.init(options, element). This creates an <iframe> pointing at the built webview app, performs a config handshake with the iframe, and resolves with the instance object ({ UI, Core, docViewer }) captured from iframe.contentWindow.instance. See Getting Started.
packages/webview A Vue 3 + Pinia SPA. It owns all UI state (which panels are open, the active tool, theme, toolbar definition) and renders the entire interface. It never imports packages/core directly — it talks to the engine exclusively through the core bridge.
Subsystems:
src/apis/ — the public API surface assembled onto window.instance.UI (and the engine surface onto window.instance.Core).src/core/ — the stateless bridge.src/components/ — ~200+ Vue components.src/stores/ — Pinia stores (viewer, document).src/helpers/, src/hooks/, src/constants/.src/core/ A flat facade of ~120 single-function modules that forward calls to the ComPDFKitViewer engine instance. It holds no business state and performs no DOM operations — its only mutable structure is the documentViewers Map (the multi-instance registry). See Core Bridge.
packages/core The ComPDFKitViewer class (src/index.js, ~5400 lines) is the monolithic heart of the SDK. It manages PDF rendering, annotations, forms, content editing, and measurement, and communicates with the WASM worker via a MessageHandler. It builds via Rollup into packages/webview/lib/webview.min.js (main library) and lib/PDFWorker.js (worker), which the webview imports at build time.
The end-to-end load sequence:
1. Integrator: ComPDFKitViewer.init(options, el)
2. webviewer.js: create iframe → webviewer/index.html?version=...#d=...&docId=...
3. iframe boots: main.js → createApp(App= Suspenser.vue) → setupStore + i18n → mount('#app')
4. Suspenser.vue renders <App/> inside <Suspense> (fallback: loading logo)
5. App/index.vue setup():
await i18nextPromise // translations ready
initDocument({ $t }) // new ComPDFKitViewer(options) from lib/webview.min.js
// → core.setDocumentViewer(1, documentViewer)
defineWebViewerInstanceUIAPIs() // window.instance = { docViewer, Core, UI }
await loadConfig(initOptions) // iframe↔parent handshake, apply integrator options
read hash params: css / theme / header
6. App onMounted: postMessage({ type: 'viewerLoaded', docId }) → window.parent
7. webviewer.js: capture iframe.contentWindow.instance, fire 'ready', resolve init promise
main.jsapplies both plugins to one app:setupStore(app)(Pinia) andi18n(app)(i18next-vue) are applied to the samecreateApp(App)instance, which is then mounted. (An earlier version created a secondcreateApp(App)for i18n and mounted that instead, leaving the Pinia-enabled app unmounted — this has been fixed.)
instance object Defined in packages/webview/src/apis/index.js:
window.instance = {
docViewer, // = core.getDocumentViewer() (instance 1)
Core: objForWebViewerCore, // spread of the entire core bridge
UI: objForWebViewerUI, // all apis/*.js factories, store-injected
};objForWebViewerUI is built from apis/ modules. Each module is a factory (stores...) => (...args) => result; index.js injects the Pinia stores at registration time, so the public surface is the inner function. Two store-access patterns coexist:
useViewer, useDocument): the API calls store actions/getters directly. Most APIs use this.viewerStore, documentStore via api_helpers/createEncapsulatedStore.js): exposes only dispatch(action)(args) and getState(key) (returns a deep clone). Used by the popup APIs, getPassword, and getPanels.The bridge supports multiple simultaneous PDF viewers via a single Map in src/core/documentViewers.js:
const documentViewerMap = new Map();
export const getDocumentViewer = (number = 1) => documentViewerMap.get(number);
export const setDocumentViewer = (number, documentViewer, options) => { /* … */ };
export const deleteDocumentViewer = (number) => { documentViewerMap.delete(number); };1 — the primary document. getDocumentViewer() with no argument resolves to instance 1.CompareDocumentContainer.vue.docNum parameter through; the rest are hardwired to instance 1. Multi-instance is therefore opt-in per method, not uniform across the API surface.apis/ → core/ → engine. APIs never import packages/core directly; they go through src/core/. The bridge never touches Pinia stores or the DOM.src/core/ files contain no business logic — they are pure delegation functions. One function per file; filename === function name; docNum is always the last parameter.apis/ module exports one factory, verb-prefixed (set/get/open/disable). Includes JSDoc.core.eventBus()) for cross-module communication — annotationChange, toolChanged, scalechanging, pagerendered, etc. See Events.ComPDFKitViewer.enqueue() serializes async operations to prevent race conditions on the WASM worker.document store is the UI-side mirror of engine tool/style state — setters like setActiveToolColor and setToolState write to the store and push into core.dispatch/getState on both stores are explicitly the API-access channel, keeping the public surface decoupled from Pinia internals.Some annotation types share implementation in the engine:
circle without measure maps to square.ink with measure maps to curve.ink with arc points maps to arc.These aliases matter when working with tools and annotation styles.
src/core/ facade in depth.