kviewer
v0.2.2
Published
Kabema PDF Editor
Readme
Kabema PDF Editor
A Nuxt module for viewing, annotating, and exporting PDFs. Built on pdfjs-dist, Konva, and pdf-lib.
Features
- 📄 PDF rendering with optional text selection layer
- ✏️ 14 annotation tools: Select, Highlight, Strikeout, Underline, Free Text, Signature, Rectangle, Circle, Arrow, Cloud, Freehand, Free Highlight, Stamp, Note
- 📤 Export PDF with or without flattened annotations
- 💾 Import/export annotation state for draft saving
- 🔄 Native PDF annotations are auto-imported into editable Konva annotations (core set: Text/FreeText/Highlight/Underline/StrikeOut/Square/Circle/Ink/Line)
- 📝 Interactive form fields (text, checkbox, radio, dropdown, signature, button)
- 🔍 Full-text search with match highlighting
- ⚡ Virtual scrolling for large documents
- ↩️ Undo/redo history
- 🗂️ Multi-tab document support (
KViewerTabs) - 🎨 Customizable header and footer slots
- 👁️ Read tracking —
allPagesRead()+all-pages-readevent to gate "must read all pages" signing
Quick Setup
Install the module:
pnpm add kviewerAdd it to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['kviewer'],
css: ['~/assets/css/main.css'],
kviewer: {
prefix: 'K', // component prefix (default)
},
})KViewer's UI is built on Nuxt UI + Tailwind CSS. Create the main stylesheet with three imports:
/* assets/css/main.css */
@import 'tailwindcss';
@import '@nuxt/ui';
@import 'kviewer';@nuxt/ui is installed and registered automatically — you don't add it to modules, and there's no @source path to maintain. The @import 'kviewer' line registers KViewer's components as a Tailwind source.
Basic Usage
<template>
<KViewer
:source="pdfUrl"
text-layer
user-name="Jane Doe"
/>
</template>
<script setup lang="ts">
const pdfUrl = '/documents/sample.pdf'
</script>source accepts a URL string, Uint8Array, or a pdfjs-dist source object.
Customizing the toolbar
By default the toolbar is fully built-in. Pass tools to choose which buttons render and in what order, and interleave custom buttons via #tool-<name> slots. Each entry is a built-in tool ID (a string) or a { type: 'slot' } object. When omitted, the default toolbar is rendered unchanged.
<template>
<KViewer
:source="pdfUrl"
:tools="['menu', 'pageSettings', 'separator', 'zoom', { type: 'slot', key: 'save' }, 'spacer', 'search']"
>
<template #tool-save="{ state }">
<UButton icon="i-lucide-save" size="xs" variant="ghost" color="neutral" @click="save(state)" />
</template>
</KViewer>
</template>separator/spacer are layout primitives (spacer right-aligns what follows). tools controls buttons only — it never gates programmatic state.selectTool(...) access. Form-edit mode is API-only (v-model:form-edit-mode / setFormEditMode() / toggleFormEditMode()); there is no built-in toggle button. See the toolbar configuration docs for the full tool-ID list and the array that reproduces the default toolbar.
Viewer Ref API
KViewer exposes methods via a template ref:
<template>
<KViewer ref="viewerRef" :source="pdfUrl" />
</template>
<script setup lang="ts">
const viewerRef = ref(null)
// Export the PDF as bytes (or trigger a download)
const bytes = await viewerRef.value?.exportPdf()
await viewerRef.value?.exportPdf({ flatten: true, download: true })
// Save / restore annotation drafts
const draft = viewerRef.value?.getAnnotations() ?? []
await viewerRef.value?.importAnnotations(draft, { mode: 'replace' })
// Read / write form field values
const fields = viewerRef.value?.getFormFieldValues()
viewerRef.value?.setFormFieldValue('email', '[email protected]')
// Toggle form-edit mode programmatically
viewerRef.value?.setFormEditMode(true)
viewerRef.value?.toggleFormEditMode()
const isEditing = viewerRef.value?.formEditMode.value
</script>| Method | Returns |
|---|---|
| getAnnotations() | IAnnotationStore[] |
| importAnnotations(annotations, options?) | Promise<{ loaded: number; skipped: number }> |
| exportPdf(options?) | Promise<Uint8Array> |
| getFormFieldValues() | FormFieldValue[] |
| setFormFieldValue(fieldName, value) | void |
| getKonvaCanvasState() | Record<number, string> |
| formEditMode | Ref<boolean> |
| setFormEditMode(enabled) | void |
| toggleFormEditMode() | boolean (the new state) |
| allPagesRead() | boolean |
| getViewedPages() | number[] |
| resetViewedPages() | void |
exportPdf options default to { flatten: false, download: false, preserveOriginalAnnotations: false }.
Form-edit mode
KViewer exposes a form-edit mode that turns every form field into a movable/resizable widget with a property sidebar. It's also reachable from the burger menu in the toolbar, but consumers can drive it externally via prop binding or the imperative API.
Two-way binding via v-model:form-edit-mode:
<template>
<button @click="formEditMode = !formEditMode">
{{ formEditMode ? 'Exit edit mode' : 'Edit form' }}
</button>
<KViewer v-model:form-edit-mode="formEditMode" :source="pdfUrl" />
</template>
<script setup lang="ts">
const formEditMode = ref(false)
</script>Or imperatively from the ref API (setFormEditMode, toggleFormEditMode, reactive formEditMode ref).
When native PDF annotations are auto-imported into Konva, exporting with preserveOriginalAnnotations: true may duplicate annotations. Prefer preserveOriginalAnnotations: false in that workflow.
Read tracking
For "must read all pages before signing" flows, KViewer tracks which pages have entered the viewport. Gate your sign action on the all-pages-read event (push) or the allPagesRead() ref method (pull):
<template>
<KViewer
:source="pdfUrl"
@all-pages-read="canSign = true"
@update:viewed-pages="(pages) => (readCount = pages.length)"
/>
<button :disabled="!canSign">Sign</button>
</template>
<script setup lang="ts">
const canSign = ref(false)
const readCount = ref(0)
</script>A page counts as read once its top edge scrolls into view (computed from geometry, so a fast scroll to the bottom still marks every page it passed). The viewer deliberately does not enforce that the user dwelled on each page — skipping is the signer's choice. The host app owns the policy (e.g. an extra "I have read all pages" checkbox); the viewer only reports the status. Call resetViewedPages() to restart tracking; loading a new source resets it automatically.
The same surface is available over the embed bridge: client.allPagesRead(), client.getViewedPages(), client.resetViewedPages(), and the all-pages-read / viewedPages-changed events via client.on(...).
Development
# Install dependencies
pnpm install
# Generate type stubs
pnpm dev:prepare
# Develop with the playground
pnpm dev
# Build the playground
pnpm dev:build
# Run ESLint
pnpm lint
# Run Vitest
pnpm test
pnpm test:watch