kviewer 0.0.10 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +29 -0
  2. package/dist/module.d.mts +9 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -1
  5. package/dist/runtime/annotation/checkbox-styles.d.ts +19 -0
  6. package/dist/runtime/annotation/checkbox-styles.js +27 -0
  7. package/dist/runtime/annotation/engine/config.js +44 -0
  8. package/dist/runtime/annotation/engine/painter.d.ts +12 -2
  9. package/dist/runtime/annotation/engine/painter.js +36 -6
  10. package/dist/runtime/annotation/engine/tools/form-field.d.ts +30 -0
  11. package/dist/runtime/annotation/engine/tools/form-field.js +119 -0
  12. package/dist/runtime/annotation/engine/types.d.ts +118 -1
  13. package/dist/runtime/annotation/engine/types.js +4 -0
  14. package/dist/runtime/annotation/font-style.d.ts +23 -0
  15. package/dist/runtime/annotation/font-style.js +57 -0
  16. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +21 -0
  17. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +75 -0
  18. package/dist/runtime/annotation/parsers/parseFormFields.js +16 -3
  19. package/dist/runtime/annotation/pdf-export/export-form-fields.d.ts +8 -5
  20. package/dist/runtime/annotation/pdf-export/export-form-fields.js +295 -1
  21. package/dist/runtime/annotation/pdf-export/export.d.ts +3 -2
  22. package/dist/runtime/annotation/pdf-export/export.js +9 -2
  23. package/dist/runtime/assets/kviewer.css +1 -1
  24. package/dist/runtime/components/FormFieldLayer.d.vue.ts +5 -0
  25. package/dist/runtime/components/FormFieldLayer.vue +40 -3
  26. package/dist/runtime/components/FormFieldLayer.vue.d.ts +5 -0
  27. package/dist/runtime/components/PdfPage.vue +30 -1
  28. package/dist/runtime/components/Viewer.d.vue.ts +43 -4
  29. package/dist/runtime/components/Viewer.vue +106 -7
  30. package/dist/runtime/components/Viewer.vue.d.ts +43 -4
  31. package/dist/runtime/components/ViewerBar.vue +24 -2
  32. package/dist/runtime/components/ViewerTabs.d.vue.ts +15 -6
  33. package/dist/runtime/components/ViewerTabs.vue +7 -2
  34. package/dist/runtime/components/ViewerTabs.vue.d.ts +15 -6
  35. package/dist/runtime/components/form-fields/FormButton.vue +16 -4
  36. package/dist/runtime/components/form-fields/FormCheckbox.vue +31 -15
  37. package/dist/runtime/components/form-fields/FormDropdown.vue +3 -1
  38. package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +12 -1
  39. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +53 -10
  40. package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +12 -1
  41. package/dist/runtime/components/form-fields/FormRadioButton.vue +3 -1
  42. package/dist/runtime/components/form-fields/FormSignatureField.vue +2 -1
  43. package/dist/runtime/components/form-fields/FormTextField.vue +47 -7
  44. package/dist/runtime/components/form-fields/PlacedFieldChrome.d.vue.ts +16 -0
  45. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +131 -0
  46. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue.d.ts +16 -0
  47. package/dist/runtime/components/modals/SignatureDrawModal.vue +70 -15
  48. package/dist/runtime/components/panels/PlacedFieldSidebar.d.vue.ts +3 -0
  49. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +505 -0
  50. package/dist/runtime/components/panels/PlacedFieldSidebar.vue.d.ts +3 -0
  51. package/dist/runtime/components/tools/FormFieldTools.d.vue.ts +3 -0
  52. package/dist/runtime/components/tools/FormFieldTools.vue +35 -0
  53. package/dist/runtime/components/tools/FormFieldTools.vue.d.ts +3 -0
  54. package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
  55. package/dist/runtime/composables/useAnnotationEngine.js +3 -1
  56. package/dist/runtime/composables/useFormFields.d.ts +48 -2
  57. package/dist/runtime/composables/useFormFields.js +338 -6
  58. package/dist/runtime/composables/useInertiaPanzoom.js +4 -0
  59. package/dist/runtime/composables/usePageVirtualization.d.ts +6 -0
  60. package/dist/runtime/composables/usePageVirtualization.js +11 -3
  61. package/dist/runtime/composables/useViewerState.d.ts +3 -0
  62. package/dist/runtime/composables/useViewerState.js +22 -1
  63. package/dist/runtime/public-types.d.ts +1 -1
  64. package/dist/types.d.mts +1 -1
  65. package/package.json +2 -2
@@ -11,7 +11,11 @@
11
11
  transform: `scale(${props.scale})`,
12
12
  transformOrigin: '0 0',
13
13
  zIndex: props.zIndex,
14
- pointerEvents: props.pointerEvents
14
+ pointerEvents: props.pointerEvents,
15
+ // Force native form controls (inputs, checkboxes, radios, scrollbars)
16
+ // to render in light mode regardless of the user\'s OS theme — PDF
17
+ // pages have a white background, so dark-mode controls clash.
18
+ colorScheme: 'light'
15
19
  }"
16
20
  >
17
21
  <FormFieldWrapper
@@ -19,13 +23,20 @@
19
23
  :key="field.id"
20
24
  :field="field"
21
25
  :page-height="props.pageHeight"
26
+ :scale="props.scale"
27
+ :view-offset-x="props.viewOffsetX ?? 0"
28
+ :view-offset-y="props.viewOffsetY ?? 0"
29
+ :selected="selectedFieldId === field.id"
30
+ @select="formFields.selectPlacedField(field.id)"
22
31
  />
23
32
  </div>
24
33
  </template>
25
34
 
26
35
  <script setup>
27
- import { computed } from "vue";
36
+ import { computed, onMounted, onBeforeUnmount, watch } from "vue";
28
37
  import { useFormFields } from "../composables/useFormFields";
38
+ import { useViewerState } from "../composables/useViewerState";
39
+ import { AnnotationType } from "../annotation/engine/types";
29
40
  import FormFieldWrapper from "./form-fields/FormFieldWrapper.vue";
30
41
  const props = defineProps({
31
42
  pageNumber: { type: Number, required: true },
@@ -33,8 +44,34 @@ const props = defineProps({
33
44
  pageWidth: { type: Number, required: true },
34
45
  pageHeight: { type: Number, required: true },
35
46
  pointerEvents: { type: String, required: true },
36
- zIndex: { type: Number, required: true }
47
+ zIndex: { type: Number, required: true },
48
+ viewOffsetX: { type: Number, required: false },
49
+ viewOffsetY: { type: Number, required: false }
37
50
  });
38
51
  const formFields = useFormFields();
52
+ const state = useViewerState();
39
53
  const fields = computed(() => formFields.getFieldsForPage(props.pageNumber));
54
+ const selectedFieldId = formFields.selectedPlacedFieldId;
55
+ watch(() => state.formEditMode.value, (enabled) => {
56
+ if (!enabled) formFields.selectPlacedField(null);
57
+ });
58
+ watch(() => state.activeTool.value, (tool) => {
59
+ const isFormFieldTool = typeof tool === "number" && (tool === AnnotationType.FORM_TEXT || tool === AnnotationType.FORM_CHECKBOX || tool === AnnotationType.FORM_RADIO || tool === AnnotationType.FORM_SIGNATURE);
60
+ if (tool !== "hand" && !isFormFieldTool) formFields.selectPlacedField(null);
61
+ });
62
+ function onKeyDown(e) {
63
+ if (!selectedFieldId.value) return;
64
+ if (e.key !== "Delete" && e.key !== "Backspace") return;
65
+ const target = e.target;
66
+ const tag = target?.tagName.toLowerCase();
67
+ if (tag === "input" || tag === "textarea" || target?.isContentEditable) return;
68
+ const id = selectedFieldId.value;
69
+ formFields.removePlacedField(id);
70
+ }
71
+ onMounted(() => {
72
+ window.addEventListener("keydown", onKeyDown);
73
+ });
74
+ onBeforeUnmount(() => {
75
+ window.removeEventListener("keydown", onKeyDown);
76
+ });
40
77
  </script>
@@ -5,6 +5,11 @@ type __VLS_Props = {
5
5
  pageHeight: number;
6
6
  pointerEvents: string;
7
7
  zIndex: number;
8
+ /** PDF user-space origin offsets (`view[0]`, `view[1]`). Forwarded to
9
+ * FormFieldWrapper so /Rect coordinates can be mapped to canvas space
10
+ * when the page's MediaBox is not at (0, 0). */
11
+ viewOffsetX?: number;
12
+ viewOffsetY?: number;
8
13
  };
9
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
15
  declare const _default: typeof __VLS_export;
@@ -31,6 +31,8 @@
31
31
  :scale="props.scale"
32
32
  :page-width="baseWidth"
33
33
  :page-height="baseHeight"
34
+ :view-offset-x="viewOffsetX"
35
+ :view-offset-y="viewOffsetY"
34
36
  :pointer-events="formFieldPointerEvents"
35
37
  :z-index="formFieldZIndex"
36
38
  />
@@ -52,6 +54,7 @@
52
54
 
53
55
  <script setup>
54
56
  import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from "vue";
57
+ import { useRuntimeConfig } from "#imports";
55
58
  import { useViewerState } from "../composables/useViewerState";
56
59
  import { useViewerSearch } from "../composables/useViewerSearch";
57
60
  import { usePageProxyCache } from "../composables/usePageProxyCache";
@@ -69,6 +72,10 @@ const props = defineProps({
69
72
  const canvasRef = ref(null);
70
73
  const textLayerRef = ref(null);
71
74
  const annotationRef = ref(null);
75
+ const runtimeConfig = useRuntimeConfig();
76
+ const minRenderPixelRatio = Number(
77
+ runtimeConfig.public.kviewer?.minRenderPixelRatio ?? 2
78
+ );
72
79
  const state = useViewerState();
73
80
  const search = useViewerSearch();
74
81
  const proxyCache = usePageProxyCache();
@@ -79,6 +86,8 @@ let currentRenderTask = null;
79
86
  let textLayer = null;
80
87
  const baseWidth = props.pageMeta.width;
81
88
  const baseHeight = props.pageMeta.height;
89
+ const viewOffsetX = props.pageMeta.viewOffsetX ?? 0;
90
+ const viewOffsetY = props.pageMeta.viewOffsetY ?? 0;
82
91
  const cssWidth = computed(() => baseWidth * props.scale);
83
92
  const cssHeight = computed(() => baseHeight * props.scale);
84
93
  const isAnnotating = computed(() => {
@@ -125,6 +134,26 @@ onMounted(async () => {
125
134
  }
126
135
  }
127
136
  });
137
+ watch(
138
+ () => state.shapeDetection.value,
139
+ async (enabled) => {
140
+ if (!enabled || !pageProxy.value) return;
141
+ const viewport = pageProxy.value.getViewport({ scale: props.scale });
142
+ const shapes = await shapeDetection.preprocessShapesForPage(
143
+ props.pageNumber,
144
+ pageProxy.value,
145
+ viewport
146
+ );
147
+ if (shapes.length > 0) {
148
+ formFields.registerDetectedCheckboxes(
149
+ props.pageNumber,
150
+ shapes,
151
+ props.scale,
152
+ baseHeight
153
+ );
154
+ }
155
+ }
156
+ );
128
157
  watch(
129
158
  () => props.scale,
130
159
  async () => {
@@ -157,7 +186,7 @@ async function renderCanvas(opts) {
157
186
  currentRenderTask.cancel();
158
187
  currentRenderTask = null;
159
188
  }
160
- const dpr = window.devicePixelRatio || 1;
189
+ const dpr = Math.max(window.devicePixelRatio || 1, minRenderPixelRatio);
161
190
  const quality = props.renderQuality ?? 1;
162
191
  const viewport = proxy.getViewport({
163
192
  scale: props.scale * quality * dpr
@@ -1,5 +1,5 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
- import type { IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
2
+ import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
3
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
4
  type __VLS_Props = {
5
5
  source: string | Uint8Array | object;
@@ -17,6 +17,20 @@ type __VLS_Props = {
17
17
  active?: boolean;
18
18
  /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
19
  freehandGroupingDelay?: number;
20
+ /** Whether the viewer is in form-edit mode: every form field renders
21
+ * with selection chrome, can be moved/resized, and the property panel
22
+ * is shown. Supports v-model via `v-model:form-edit-mode`. */
23
+ formEditMode?: boolean;
24
+ /** Map of roleId → CSS color used to color-code form fields in
25
+ * form-edit mode. The editor stays unopinionated about role
26
+ * definitions: the host owns the role list, names, and color picker,
27
+ * and just hands a flat lookup table to the viewer. Unknown or
28
+ * missing ids fall back to the default blue. */
29
+ roleColors?: Record<string, string>;
30
+ /** Active role id new field placements are auto-tagged with. The host
31
+ * drives this — typically by binding it to a role picker in its own
32
+ * UI. Supports v-model via `v-model:active-role-id`. */
33
+ activeRoleId?: string | null;
20
34
  };
21
35
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
22
36
  type ImportMode = 'replace' | 'merge';
@@ -27,11 +41,11 @@ declare function importAnnotations(annotations: IAnnotationStore[], options?: {
27
41
  skipped: number;
28
42
  }>;
29
43
  declare function getKonvaCanvasState(): Record<number, string>;
30
- declare var __VLS_1: {}, __VLS_40: {};
44
+ declare var __VLS_1: {}, __VLS_51: {};
31
45
  type __VLS_Slots = {} & {
32
46
  header?: (props: typeof __VLS_1) => any;
33
47
  } & {
34
- footer?: (props: typeof __VLS_40) => any;
48
+ footer?: (props: typeof __VLS_51) => any;
35
49
  };
36
50
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
37
51
  getAnnotations: () => IAnnotationStore[];
@@ -40,7 +54,29 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
54
  getKonvaCanvasState: typeof getKonvaCanvasState;
41
55
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
42
56
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
43
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
57
+ /** Add a form field programmatically. Returns the created definition.
58
+ * The new field is exported into the PDF on `exportPdf()` like any
59
+ * field placed via the placement tool. */
60
+ addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
61
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
62
+ updateFormField: (id: string, patch: Partial<FormFieldDefinition>) => void;
63
+ /** Remove a form field by id (drops its value too). */
64
+ removeFormField: (id: string) => void;
65
+ /** All form-field definitions (parsed, detected, and placed), flat. */
66
+ getFormFields: () => FormFieldDefinition[];
67
+ /** Reactive form-edit-mode flag — read with `.value`. */
68
+ formEditMode: import("vue").Ref<boolean, boolean>;
69
+ /** Programmatically enter or leave form-edit mode. */
70
+ setFormEditMode: (enabled: boolean) => void;
71
+ /** Flip form-edit mode. Returns the new state. */
72
+ toggleFormEditMode: () => boolean;
73
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
74
+ "update:formEditMode": (value: boolean) => any;
75
+ "update:activeRoleId": (value: string | null) => any;
76
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
77
+ "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
78
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
79
+ }>, {
44
80
  userName: string;
45
81
  freehandGroupingDelay: number;
46
82
  zoom: number;
@@ -50,6 +86,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
50
86
  signatureHandlers: SignatureHandlers;
51
87
  viewMode: ViewMode;
52
88
  shapeDetection: boolean;
89
+ formEditMode: boolean;
90
+ roleColors: Record<string, string>;
91
+ activeRoleId: string | null;
53
92
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
54
93
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
55
94
  declare const _default: typeof __VLS_export;
@@ -5,7 +5,8 @@
5
5
  <ViewerBar />
6
6
  </slot>
7
7
 
8
- <div class="relative flex-1 min-h-0">
8
+ <div class="flex flex-1 min-h-0">
9
+ <div class="relative flex-1 min-w-0">
9
10
  <div
10
11
  ref="scrollContainer"
11
12
  class="absolute inset-0 overflow-hidden bg-[#F0F3F5] dark:bg-muted/50"
@@ -133,6 +134,11 @@
133
134
  <ClientOnly>
134
135
  <FloatingPageIndicator :scroll-container="scrollContainer" />
135
136
  </ClientOnly>
137
+ </div>
138
+
139
+ <ClientOnly v-if="!viewerState.readonly.value">
140
+ <PlacedFieldSidebar />
141
+ </ClientOnly>
136
142
  </div>
137
143
 
138
144
  <!-- Footer slot: empty by default -->
@@ -180,6 +186,7 @@ import {
180
186
  import ViewerBar from "./ViewerBar.vue";
181
187
  import PdfPage from "./PdfPage.vue";
182
188
  import FreeTextModal from "./modals/FreeTextModal.vue";
189
+ import PlacedFieldSidebar from "./panels/PlacedFieldSidebar.vue";
183
190
  import AnnotationToolbar from "./AnnotationToolbar.vue";
184
191
  import FloatingPageIndicator from "./FloatingPageIndicator.vue";
185
192
  import { providePageSettings } from "../composables/usePageSettings";
@@ -199,6 +206,7 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
199
206
  import { createSearchIndex } from "../composables/useSearchIndex";
200
207
  import { createShapeDetection } from "../composables/useShapeDetection";
201
208
  import { provideFormFields } from "../composables/useFormFields";
209
+ import { extractCheckboxStyles, extractButtonCaptions } from "../annotation/parsers/extractCheckboxStyles";
202
210
  import { isIPad } from "../annotation/engine/input-device";
203
211
  import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
204
212
  const props = defineProps({
@@ -212,8 +220,12 @@ const props = defineProps({
212
220
  readonly: { type: Boolean, required: false, default: false },
213
221
  shapeDetection: { type: Boolean, required: false, default: false },
214
222
  active: { type: Boolean, required: false, default: true },
215
- freehandGroupingDelay: { type: Number, required: false, default: 1e3 }
223
+ freehandGroupingDelay: { type: Number, required: false, default: 1e3 },
224
+ formEditMode: { type: Boolean, required: false, default: void 0 },
225
+ roleColors: { type: Object, required: false, default: void 0 },
226
+ activeRoleId: { type: [String, null], required: false, default: void 0 }
216
227
  });
228
+ const emit = defineEmits(["update:formEditMode", "update:activeRoleId"]);
217
229
  const viewerRoot = ref(null);
218
230
  const scrollContainer = ref(null);
219
231
  const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
@@ -223,8 +235,45 @@ watchEffect(() => {
223
235
  watchEffect(() => {
224
236
  viewerState.shapeDetection.value = props.shapeDetection ?? false;
225
237
  });
238
+ watch(
239
+ () => props.formEditMode,
240
+ (v) => {
241
+ if (v === void 0) return;
242
+ if (viewerState.formEditMode.value !== v) viewerState.formEditMode.value = v;
243
+ },
244
+ { immediate: true }
245
+ );
246
+ watch(
247
+ () => viewerState.formEditMode.value,
248
+ (v) => {
249
+ if (props.formEditMode !== v) emit("update:formEditMode", v);
250
+ }
251
+ );
226
252
  const viewerSearch = provideViewerSearch();
227
253
  const formFieldsState = provideFormFields();
254
+ watch(
255
+ () => props.roleColors,
256
+ (v) => {
257
+ formFieldsState.setRoleColors(v ?? {});
258
+ },
259
+ { immediate: true }
260
+ );
261
+ watch(
262
+ () => props.activeRoleId,
263
+ (v) => {
264
+ if (v === void 0) return;
265
+ if (formFieldsState.activeRoleId.value !== v) {
266
+ formFieldsState.setActiveRole(v);
267
+ }
268
+ },
269
+ { immediate: true }
270
+ );
271
+ watch(
272
+ () => formFieldsState.activeRoleId.value,
273
+ (v) => {
274
+ if (props.activeRoleId !== v) emit("update:activeRoleId", v);
275
+ }
276
+ );
228
277
  const pageSettings = providePageSettings(viewerRoot);
229
278
  const virtualization = createPageVirtualization();
230
279
  setScrollToPageFn((pageNumber) => {
@@ -368,14 +417,28 @@ async function loadDocument() {
368
417
  pdfjs.GlobalWorkerOptions.workerSrc = workerModule.default;
369
418
  }
370
419
  const source = props.source;
371
- const loadingTask = pdfjs.getDocument(
372
- source
373
- );
420
+ const baseParams = source instanceof Uint8Array || typeof source === "string" ? { url: typeof source === "string" ? source : void 0, data: source instanceof Uint8Array ? source : void 0 } : { ...source };
421
+ const docParams = {
422
+ ...baseParams,
423
+ standardFontDataUrl: "/_kviewer/standard_fonts/",
424
+ cMapUrl: "/_kviewer/cmaps/",
425
+ cMapPacked: true,
426
+ useSystemFonts: true
427
+ };
428
+ const loadingTask = pdfjs.getDocument(docParams);
374
429
  const pdfDoc = await loadingTask.promise;
375
430
  doc.value = pdfDoc;
376
431
  viewerState.doc.value = pdfDoc;
377
432
  viewerState.totalPages.value = pdfDoc.numPages;
378
433
  proxyCache.setDocument(pdfDoc);
434
+ pdfDoc.getData().then(async (bytes) => {
435
+ const [styles, captions] = await Promise.all([
436
+ extractCheckboxStyles(bytes),
437
+ extractButtonCaptions(bytes)
438
+ ]);
439
+ if (styles.size > 0) formFieldsState.applyCheckboxStyles(styles);
440
+ if (captions.size > 0) formFieldsState.applyButtonCaptions(captions);
441
+ }).catch((err) => console.warn("[KViewer] Failed to read MK data from source PDF:", err));
379
442
  if (!scrollContainer.value) {
380
443
  await nextTick();
381
444
  }
@@ -487,11 +550,13 @@ async function exportPdf(options = {}) {
487
550
  const pdfData = await doc.value.getData();
488
551
  const exportContext = painter.getExportContext();
489
552
  const formFieldValues = formFieldsState.getAllFieldValues();
553
+ const placedFormFieldDefinitions = formFieldsState.getPlacedFields();
490
554
  const bytes = await exportAnnotationsToPdf({
491
555
  pdfData,
492
556
  annotations: exportContext.annotations,
493
557
  options: mergedOptions,
494
558
  formFieldValues,
559
+ placedFormFieldDefinitions,
495
560
  originalAnnotationIds: exportContext.originalIds,
496
561
  modifiedAnnotationIds: exportContext.modifiedIds,
497
562
  deletedAnnotationIds: exportContext.deletedIds
@@ -642,6 +707,15 @@ onMounted(() => {
642
707
  painter = createAnnotationEngine(viewerState, {
643
708
  userName: props.userName,
644
709
  onRequestTextInput,
710
+ onPlaceField: (payload) => {
711
+ formFieldsState.addPlacedField(payload);
712
+ viewerState.selectTool("hand");
713
+ },
714
+ getFormFieldPreviewColor: () => {
715
+ const id = formFieldsState.activeRoleId.value;
716
+ if (!id) return void 0;
717
+ return formFieldsState.roleColors.value[id];
718
+ },
645
719
  freehandGroupingDelay: props.freehandGroupingDelay,
646
720
  // Suspend panzoom gestures while an annotation is being dragged or
647
721
  // transformed. Otherwise single-finger pan runs in parallel with the
@@ -670,6 +744,8 @@ onMounted(() => {
670
744
  inertiaPanzoom.onChange(() => {
671
745
  if (viewerState.selectionRect.value) viewerState.selectionRect.value = null;
672
746
  if (!isSinglePageMode.value) virtualization.updateCurrentPageByRect();
747
+ const z = inertiaPanzoom.getScale();
748
+ if (z !== viewerState.visualZoom.value) viewerState.visualZoom.value = z;
673
749
  });
674
750
  nextTick(() => initPanzoom());
675
751
  watch(
@@ -684,7 +760,10 @@ onMounted(() => {
684
760
  },
685
761
  { immediate: true }
686
762
  );
687
- watch(() => viewerState.scale.value, () => inertiaPanzoom.reset());
763
+ watch(() => viewerState.scale.value, async () => {
764
+ await nextTick();
765
+ inertiaPanzoom.reset();
766
+ });
688
767
  watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
689
768
  }
690
769
  if (isIPad()) {
@@ -752,7 +831,27 @@ defineExpose({
752
831
  exportPdf,
753
832
  getKonvaCanvasState,
754
833
  getFormFieldValues: () => formFieldsState.getAllFieldValues(),
755
- setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value)
834
+ setFormFieldValue: (fieldName, value) => formFieldsState.setFieldValueByName(fieldName, value),
835
+ /** Add a form field programmatically. Returns the created definition.
836
+ * The new field is exported into the PDF on `exportPdf()` like any
837
+ * field placed via the placement tool. */
838
+ addFormField: (payload) => formFieldsState.addFormField(payload),
839
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
840
+ updateFormField: (id, patch) => formFieldsState.updatePlacedField(id, patch),
841
+ /** Remove a form field by id (drops its value too). */
842
+ removeFormField: (id) => formFieldsState.removePlacedField(id),
843
+ /** All form-field definitions (parsed, detected, and placed), flat. */
844
+ getFormFields: () => formFieldsState.getAllFields(),
845
+ /** Reactive form-edit-mode flag — read with `.value`. */
846
+ formEditMode: viewerState.formEditMode,
847
+ /** Programmatically enter or leave form-edit mode. */
848
+ setFormEditMode: (enabled) => viewerState.setFormEditMode(enabled),
849
+ /** Flip form-edit mode. Returns the new state. */
850
+ toggleFormEditMode: () => {
851
+ const next = !viewerState.formEditMode.value;
852
+ viewerState.setFormEditMode(next);
853
+ return next;
854
+ }
756
855
  });
757
856
  </script>
758
857
 
@@ -1,5 +1,5 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
- import type { IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
2
+ import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
3
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
4
  type __VLS_Props = {
5
5
  source: string | Uint8Array | object;
@@ -17,6 +17,20 @@ type __VLS_Props = {
17
17
  active?: boolean;
18
18
  /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
19
  freehandGroupingDelay?: number;
20
+ /** Whether the viewer is in form-edit mode: every form field renders
21
+ * with selection chrome, can be moved/resized, and the property panel
22
+ * is shown. Supports v-model via `v-model:form-edit-mode`. */
23
+ formEditMode?: boolean;
24
+ /** Map of roleId → CSS color used to color-code form fields in
25
+ * form-edit mode. The editor stays unopinionated about role
26
+ * definitions: the host owns the role list, names, and color picker,
27
+ * and just hands a flat lookup table to the viewer. Unknown or
28
+ * missing ids fall back to the default blue. */
29
+ roleColors?: Record<string, string>;
30
+ /** Active role id new field placements are auto-tagged with. The host
31
+ * drives this — typically by binding it to a role picker in its own
32
+ * UI. Supports v-model via `v-model:active-role-id`. */
33
+ activeRoleId?: string | null;
20
34
  };
21
35
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
22
36
  type ImportMode = 'replace' | 'merge';
@@ -27,11 +41,11 @@ declare function importAnnotations(annotations: IAnnotationStore[], options?: {
27
41
  skipped: number;
28
42
  }>;
29
43
  declare function getKonvaCanvasState(): Record<number, string>;
30
- declare var __VLS_1: {}, __VLS_40: {};
44
+ declare var __VLS_1: {}, __VLS_51: {};
31
45
  type __VLS_Slots = {} & {
32
46
  header?: (props: typeof __VLS_1) => any;
33
47
  } & {
34
- footer?: (props: typeof __VLS_40) => any;
48
+ footer?: (props: typeof __VLS_51) => any;
35
49
  };
36
50
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
37
51
  getAnnotations: () => IAnnotationStore[];
@@ -40,7 +54,29 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
54
  getKonvaCanvasState: typeof getKonvaCanvasState;
41
55
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
42
56
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
43
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
57
+ /** Add a form field programmatically. Returns the created definition.
58
+ * The new field is exported into the PDF on `exportPdf()` like any
59
+ * field placed via the placement tool. */
60
+ addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
61
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
62
+ updateFormField: (id: string, patch: Partial<FormFieldDefinition>) => void;
63
+ /** Remove a form field by id (drops its value too). */
64
+ removeFormField: (id: string) => void;
65
+ /** All form-field definitions (parsed, detected, and placed), flat. */
66
+ getFormFields: () => FormFieldDefinition[];
67
+ /** Reactive form-edit-mode flag — read with `.value`. */
68
+ formEditMode: import("vue").Ref<boolean, boolean>;
69
+ /** Programmatically enter or leave form-edit mode. */
70
+ setFormEditMode: (enabled: boolean) => void;
71
+ /** Flip form-edit mode. Returns the new state. */
72
+ toggleFormEditMode: () => boolean;
73
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
74
+ "update:formEditMode": (value: boolean) => any;
75
+ "update:activeRoleId": (value: string | null) => any;
76
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
77
+ "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
78
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
79
+ }>, {
44
80
  userName: string;
45
81
  freehandGroupingDelay: number;
46
82
  zoom: number;
@@ -50,6 +86,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
50
86
  signatureHandlers: SignatureHandlers;
51
87
  viewMode: ViewMode;
52
88
  shapeDetection: boolean;
89
+ formEditMode: boolean;
90
+ roleColors: Record<string, string>;
91
+ activeRoleId: string | null;
53
92
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
54
93
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
55
94
  declare const _default: typeof __VLS_export;
@@ -5,7 +5,7 @@
5
5
  <UPopover>
6
6
  <UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" data-testid="viewer-menu" />
7
7
  <template #content>
8
- <div class="p-1">
8
+ <div class="p-1 flex flex-col gap-0.5">
9
9
  <UButton
10
10
  icon="i-lucide-download"
11
11
  label="Download"
@@ -16,6 +16,16 @@
16
16
  data-testid="viewer-download"
17
17
  @click="onDownload"
18
18
  />
19
+ <UButton
20
+ :icon="state.shapeDetection.value ? 'i-lucide-square-check-big' : 'i-lucide-square-dashed'"
21
+ :label="state.shapeDetection.value ? 'Disable form field detection' : 'Detect form fields'"
22
+ :variant="state.shapeDetection.value ? 'soft' : 'ghost'"
23
+ :color="state.shapeDetection.value ? 'primary' : 'neutral'"
24
+ size="xs"
25
+ class="w-full justify-start"
26
+ data-testid="viewer-toggle-shape-detection"
27
+ @click="state.shapeDetection.value = !state.shapeDetection.value"
28
+ />
19
29
  </div>
20
30
  </template>
21
31
  </UPopover>
@@ -30,6 +40,16 @@
30
40
 
31
41
  <HandTool />
32
42
  <MarqueeTool />
43
+
44
+ <UButton
45
+ icon="i-lucide-form-input"
46
+ :variant="state.formEditMode.value ? 'soft' : 'ghost'"
47
+ :color="state.formEditMode.value ? 'primary' : 'neutral'"
48
+ size="xs"
49
+ :title="state.formEditMode.value ? 'Exit form-edit mode' : 'Enter form-edit mode'"
50
+ data-testid="form-edit-toggle"
51
+ @click="state.setFormEditMode(!state.formEditMode.value)"
52
+ />
33
53
  </template>
34
54
 
35
55
  <div class="ml-auto flex items-center gap-2">
@@ -45,7 +65,8 @@
45
65
  </div>
46
66
 
47
67
  <div class="justify-self-center">
48
- <DrawingTools :style-version="styleVersion" />
68
+ <FormFieldTools v-if="state.formEditMode.value" />
69
+ <DrawingTools v-else :style-version="styleVersion" />
49
70
  </div>
50
71
 
51
72
  <div class="justify-self-end flex items-center gap-1">
@@ -63,6 +84,7 @@ import ZoomControls from "./tools/ZoomControls.vue";
63
84
  import HandTool from "./tools/HandTool.vue";
64
85
  import MarqueeTool from "./tools/MarqueeTool.vue";
65
86
  import DrawingTools from "./tools/DrawingTools.vue";
87
+ import FormFieldTools from "./tools/FormFieldTools.vue";
66
88
  import ActionTools from "./tools/ActionTools.vue";
67
89
  import PageInfo from "./tools/PageInfo.vue";
68
90
  import SearchTool from "./tools/SearchTool.vue";
@@ -48,6 +48,11 @@ type __VLS_Props = {
48
48
  zoom?: number;
49
49
  /** Minimum number of tabs -- prevents closing below this count. */
50
50
  minTabs?: number;
51
+ /** Forwarded to every Viewer instance — see KViewer's `roleColors`. */
52
+ roleColors?: Record<string, string>;
53
+ /** Forwarded to every Viewer instance — see KViewer's `activeRoleId`.
54
+ * Supports v-model via `v-model:active-role-id`. */
55
+ activeRoleId?: string | null;
51
56
  };
52
57
  declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
53
58
  id?: string;
@@ -55,7 +60,7 @@ declare function addTab(item: Omit<ViewerTabItem, 'id'> & {
55
60
  declare function removeTab(id: string): boolean;
56
61
  declare function activateTab(id: string): void;
57
62
  declare function getViewer(id: string): ComponentPublicInstance | null;
58
- declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
63
+ declare var __VLS_7: {}, __VLS_21: {}, __VLS_32: {
59
64
  tab: {
60
65
  id: string;
61
66
  label: string;
@@ -154,7 +159,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
154
159
  zoom?: number | undefined;
155
160
  shapeDetection?: boolean | undefined;
156
161
  };
157
- }, __VLS_33: {
162
+ }, __VLS_35: {
158
163
  tab: {
159
164
  id: string;
160
165
  label: string;
@@ -253,17 +258,17 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
253
258
  zoom?: number | undefined;
254
259
  shapeDetection?: boolean | undefined;
255
260
  };
256
- }, __VLS_35: {};
261
+ }, __VLS_37: {};
257
262
  type __VLS_Slots = {} & {
258
263
  'tabs-leading'?: (props: typeof __VLS_7) => any;
259
264
  } & {
260
265
  'tabs-trailing'?: (props: typeof __VLS_21) => any;
261
266
  } & {
262
- header?: (props: typeof __VLS_30) => any;
267
+ header?: (props: typeof __VLS_32) => any;
263
268
  } & {
264
- footer?: (props: typeof __VLS_33) => any;
269
+ footer?: (props: typeof __VLS_35) => any;
265
270
  } & {
266
- empty?: (props: typeof __VLS_35) => any;
271
+ empty?: (props: typeof __VLS_37) => any;
267
272
  };
268
273
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
269
274
  addTab: typeof addTab;
@@ -370,11 +375,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
370
375
  shapeDetection?: boolean | undefined;
371
376
  }[];
372
377
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
378
+ "update:activeRoleId": (value: string | null) => any;
373
379
  "update:activeTab": (id: string) => any;
374
380
  "tab-added": (tab: ViewerTabItem) => any;
375
381
  "tab-close": (id: string) => any;
376
382
  "tab-removed": (id: string) => any;
377
383
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
384
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
378
385
  "onUpdate:activeTab"?: ((id: string) => any) | undefined;
379
386
  "onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
380
387
  "onTab-close"?: ((id: string) => any) | undefined;
@@ -385,6 +392,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
385
392
  stamps: StampDefinition[];
386
393
  signatureHandlers: SignatureHandlers;
387
394
  viewMode: ViewMode;
395
+ roleColors: Record<string, string>;
396
+ activeRoleId: string | null;
388
397
  defaultActiveTab: string;
389
398
  minTabs: number;
390
399
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;