kviewer 0.2.0 → 0.2.2

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 (114) hide show
  1. package/README.md +47 -0
  2. package/dist/module.d.mts +8 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -3
  5. package/dist/runtime/annotation/engine/canvas-resolution.d.ts +7 -0
  6. package/dist/runtime/annotation/engine/canvas-resolution.js +21 -0
  7. package/dist/runtime/annotation/engine/editor/editor.d.ts +5 -0
  8. package/dist/runtime/annotation/engine/editor/editor.js +14 -0
  9. package/dist/runtime/annotation/engine/editor/selector.d.ts +13 -1
  10. package/dist/runtime/annotation/engine/editor/selector.js +60 -8
  11. package/dist/runtime/annotation/engine/painter.d.ts +31 -0
  12. package/dist/runtime/annotation/engine/painter.js +88 -2
  13. package/dist/runtime/annotation/engine/store.js +3 -0
  14. package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
  15. package/dist/runtime/annotation/engine/tools/circle.js +0 -2
  16. package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
  17. package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
  18. package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
  19. package/dist/runtime/annotation/engine/types.d.ts +5 -0
  20. package/dist/runtime/annotation/font-style.d.ts +15 -0
  21. package/dist/runtime/annotation/font-style.js +19 -0
  22. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
  23. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
  24. package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
  25. package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
  26. package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
  27. package/dist/runtime/annotation/pdf-export/export.js +13 -1
  28. package/dist/runtime/annotation/pdf-export/parse_circle.js +3 -0
  29. package/dist/runtime/annotation/pdf-export/parse_freetext.js +31 -19
  30. package/dist/runtime/annotation/pdf-export/parse_highlight.js +3 -0
  31. package/dist/runtime/annotation/pdf-export/parse_ink.js +5 -1
  32. package/dist/runtime/annotation/pdf-export/parse_line.js +5 -1
  33. package/dist/runtime/annotation/pdf-export/parse_polyline.js +5 -1
  34. package/dist/runtime/annotation/pdf-export/parse_square.js +3 -0
  35. package/dist/runtime/annotation/pdf-export/parse_stamp.js +4 -2
  36. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +3 -0
  37. package/dist/runtime/annotation/pdf-export/parse_text.js +3 -0
  38. package/dist/runtime/annotation/pdf-export/parse_underline.js +3 -0
  39. package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
  40. package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
  41. package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
  42. package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
  43. package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
  44. package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
  45. package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
  46. package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
  47. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
  48. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
  49. package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
  50. package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
  51. package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
  52. package/dist/runtime/assets/kviewer.css +1 -1
  53. package/dist/runtime/components/AnnotationToolbar.vue +6 -4
  54. package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
  55. package/dist/runtime/components/PdfPage.vue +170 -9
  56. package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
  57. package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
  58. package/dist/runtime/components/PerfHud.vue +97 -0
  59. package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
  60. package/dist/runtime/components/ToolButton.vue +4 -1
  61. package/dist/runtime/components/Viewer.d.vue.ts +37 -2
  62. package/dist/runtime/components/Viewer.vue +208 -13
  63. package/dist/runtime/components/Viewer.vue.d.ts +37 -2
  64. package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
  65. package/dist/runtime/components/ViewerBar.vue +183 -101
  66. package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
  67. package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
  68. package/dist/runtime/components/ViewerTabs.vue +13 -2
  69. package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
  70. package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
  71. package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
  72. package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
  73. package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
  74. package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
  75. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
  76. package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
  77. package/dist/runtime/components/panels/StampPicker.vue +3 -1
  78. package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
  79. package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
  80. package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
  81. package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
  82. package/dist/runtime/components/tools/PageSettings.vue +13 -11
  83. package/dist/runtime/components/tools/SearchTool.vue +12 -10
  84. package/dist/runtime/components/tools/ToolProperties.vue +7 -5
  85. package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
  86. package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
  87. package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
  88. package/dist/runtime/components/tools/ZoomControls.vue +17 -9
  89. package/dist/runtime/composables/canvas-limits.d.ts +22 -0
  90. package/dist/runtime/composables/canvas-limits.js +10 -0
  91. package/dist/runtime/composables/useFormFields.d.ts +6 -0
  92. package/dist/runtime/composables/useFormFields.js +28 -0
  93. package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
  94. package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
  95. package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
  96. package/dist/runtime/composables/useKviewerI18n.js +29 -0
  97. package/dist/runtime/composables/usePageVirtualization.d.ts +11 -0
  98. package/dist/runtime/composables/usePageVirtualization.js +40 -0
  99. package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
  100. package/dist/runtime/composables/useScriptingManager.js +2 -0
  101. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  102. package/dist/runtime/composables/useViewerState.js +8 -0
  103. package/dist/runtime/embed/bridge-client.d.ts +9 -1
  104. package/dist/runtime/embed/bridge-client.js +13 -0
  105. package/dist/runtime/embed/bridge-host.d.ts +3 -0
  106. package/dist/runtime/embed/bridge-host.js +30 -8
  107. package/dist/runtime/embed/protocol.d.ts +21 -0
  108. package/dist/runtime/i18n/messages.d.ts +252 -0
  109. package/dist/runtime/i18n/messages.js +249 -0
  110. package/dist/runtime/public-types.d.ts +2 -0
  111. package/dist/runtime/toolbar-tools.d.ts +54 -0
  112. package/dist/runtime/toolbar-tools.js +79 -0
  113. package/dist/types.d.mts +1 -1
  114. package/package.json +14 -31
@@ -2,7 +2,11 @@
2
2
  <div ref="viewerRoot" class="flex flex-col h-full">
3
3
  <!-- Header slot: defaults to ViewerBar -->
4
4
  <slot name="header">
5
- <ViewerBar :menu-items="props.menuItems" />
5
+ <ViewerBar :menu-items="props.menuItems" :tools="props.tools">
6
+ <template v-for="name in toolSlotNames" :key="name" #[name]="scope">
7
+ <slot :name="name" v-bind="scope" />
8
+ </template>
9
+ </ViewerBar>
6
10
  </slot>
7
11
 
8
12
  <div class="flex flex-1 min-h-0">
@@ -61,6 +65,7 @@
61
65
  :page-number="singlePageMeta.pageNumber"
62
66
  :page-meta="singlePageMeta"
63
67
  :scale="pageScale"
68
+ :view-window="pageViewWindow(singlePageMeta.pageNumber)"
64
69
  />
65
70
  </div>
66
71
  </div>
@@ -89,6 +94,7 @@
89
94
  :page-number="meta.pageNumber"
90
95
  :page-meta="meta"
91
96
  :scale="pageScale"
97
+ :view-window="pageViewWindow(meta.pageNumber)"
92
98
  />
93
99
  </div>
94
100
  </div>
@@ -122,6 +128,7 @@
122
128
  :page-number="meta.pageNumber"
123
129
  :page-meta="meta"
124
130
  :scale="pageScale"
131
+ :view-window="pageViewWindow(meta.pageNumber)"
125
132
  />
126
133
  </div>
127
134
  </div>
@@ -133,6 +140,10 @@
133
140
 
134
141
  <ClientOnly>
135
142
  <FloatingPageIndicator :scroll-container="scrollContainer" />
143
+ <!-- Debug overlay for on-device profiling; renders only with
144
+ ?perf in the URL. Gated on `active` so tabbed hosts show a
145
+ single instance. -->
146
+ <PerfHud v-if="props.active" />
136
147
  </ClientOnly>
137
148
  </div>
138
149
 
@@ -181,14 +192,17 @@ import {
181
192
  nextTick,
182
193
  triggerRef,
183
194
  onMounted,
184
- onBeforeUnmount
195
+ onBeforeUnmount,
196
+ useSlots
185
197
  } from "vue";
198
+ import { useRuntimeConfig } from "#imports";
186
199
  import ViewerBar from "./ViewerBar.vue";
187
200
  import PdfPage from "./PdfPage.vue";
188
201
  import FreeTextModal from "./modals/FreeTextModal.vue";
189
202
  import PlacedFieldSidebar from "./panels/PlacedFieldSidebar.vue";
190
203
  import AnnotationToolbar from "./AnnotationToolbar.vue";
191
204
  import FloatingPageIndicator from "./FloatingPageIndicator.vue";
205
+ import PerfHud from "./PerfHud.vue";
192
206
  import { providePageSettings } from "../composables/usePageSettings";
193
207
  import { mapViewModeToFitMode, normalizeZoom, provideViewerState } from "../composables/useViewerState";
194
208
  import { provideViewerSearch } from "../composables/useViewerSearch";
@@ -206,11 +220,13 @@ import { createPageProxyCache } from "../composables/usePageProxyCache";
206
220
  import { createSearchIndex } from "../composables/useSearchIndex";
207
221
  import { createShapeDetection } from "../composables/useShapeDetection";
208
222
  import { provideFormFields } from "../composables/useFormFields";
209
- import { extractCheckboxStyles, extractButtonCaptions } from "../annotation/parsers/extractCheckboxStyles";
223
+ import { extractCheckboxStyles, extractButtonCaptions, extractSignedSignatureNames } from "../annotation/parsers/extractCheckboxStyles";
210
224
  import { isIPad } from "../annotation/engine/input-device";
211
225
  import { useInertiaPanzoom } from "../composables/useInertiaPanzoom";
212
226
  import { createScriptingManager } from "../composables/useScriptingManager";
213
227
  import { createScriptingBridge } from "../composables/useScriptingBridge";
228
+ import { provideKviewerI18n } from "../composables/useKviewerI18n";
229
+ import { KVIEWER_LOCALES } from "../i18n/messages";
214
230
  const props = defineProps({
215
231
  source: { type: null, required: true },
216
232
  textLayer: { type: Boolean, required: false },
@@ -227,12 +243,19 @@ const props = defineProps({
227
243
  roleColors: { type: Object, required: false, default: void 0 },
228
244
  activeRoleId: { type: [String, null], required: false, default: void 0 },
229
245
  scripting: { type: Boolean, required: false, default: false },
230
- menuItems: { type: Array, required: false, default: void 0 }
246
+ menuItems: { type: Array, required: false, default: void 0 },
247
+ tools: { type: Array, required: false, default: void 0 },
248
+ locale: { type: null, required: false, default: void 0 },
249
+ messages: { type: Object, required: false, default: void 0 }
231
250
  });
232
- const emit = defineEmits(["update:formEditMode", "update:activeRoleId"]);
251
+ const slots = useSlots();
252
+ const toolSlotNames = computed(
253
+ () => Object.keys(slots).filter((name) => name.startsWith("tool-"))
254
+ );
255
+ const emit = defineEmits(["update:formEditMode", "update:activeRoleId", "update:viewedPages", "all-pages-read"]);
233
256
  const viewerRoot = ref(null);
234
257
  const scrollContainer = ref(null);
235
- const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
258
+ const { state: viewerState, setScrollToPageFn, setDownloadPdfFn, setApplyScaleFn } = provideViewerState();
236
259
  watchEffect(() => {
237
260
  viewerState.readonly.value = props.readonly ?? false;
238
261
  });
@@ -255,6 +278,14 @@ watch(
255
278
  );
256
279
  const viewerSearch = provideViewerSearch();
257
280
  const formFieldsState = provideFormFields();
281
+ const kviewerPublicConfig = useRuntimeConfig().public.kviewer;
282
+ provideKviewerI18n(
283
+ computed(() => {
284
+ const candidate = props.locale ?? kviewerPublicConfig?.locale ?? "en";
285
+ return KVIEWER_LOCALES.includes(candidate) ? candidate : "en";
286
+ }),
287
+ computed(() => props.messages ?? {})
288
+ );
258
289
  watch(
259
290
  () => props.roleColors,
260
291
  (v) => {
@@ -295,6 +326,7 @@ const proxyCache = createPageProxyCache();
295
326
  const searchIndex = createSearchIndex();
296
327
  const shapeDetectionCache = createShapeDetection();
297
328
  const doc = shallowRef(null);
329
+ let destroyActiveDoc = null;
298
330
  let painter = null;
299
331
  let scriptingManager = null;
300
332
  let scriptingBridge = null;
@@ -306,6 +338,31 @@ watch(
306
338
  viewerState.currentPage.value = page;
307
339
  }
308
340
  );
341
+ watch(
342
+ () => viewerState.currentPage.value,
343
+ (page) => virtualization.markPageViewed(page),
344
+ { immediate: true }
345
+ );
346
+ watch(
347
+ () => pageMetas.value.length,
348
+ (len) => {
349
+ if (len > 0 && !isSinglePageMode.value) {
350
+ nextTick(() => virtualization.updateCurrentPageByRect());
351
+ }
352
+ }
353
+ );
354
+ watch(
355
+ virtualization.allPagesRead,
356
+ (read, prev) => {
357
+ if (read && !prev) emit("all-pages-read");
358
+ }
359
+ );
360
+ watch(
361
+ virtualization.viewedPages,
362
+ (pages) => {
363
+ emit("update:viewedPages", [...pages].sort((a, b) => a - b));
364
+ }
365
+ );
309
366
  function onPageRef(pageNumber, el) {
310
367
  if (el) {
311
368
  virtualization.observePage(pageNumber, el);
@@ -328,6 +385,116 @@ const isRotatedSideways = computed(() => {
328
385
  });
329
386
  const zoomWrapper = ref(null);
330
387
  const inertiaPanzoom = useInertiaPanzoom({ minScale: 0.5, maxScale: 4 });
388
+ const MIN_ZOOM = 0.25;
389
+ const MAX_ZOOM = 4;
390
+ let suppressScaleReset = false;
391
+ async function commitZoom(target) {
392
+ const s0 = viewerState.scale.value;
393
+ target = Math.min(Math.max(MAX_ZOOM, s0), Math.max(Math.min(MIN_ZOOM, s0), target));
394
+ const viewportEl = zoomWrapper.value?.parentElement ?? null;
395
+ const anchor = virtualization.getPageElement(viewerState.currentPage.value);
396
+ const visual = inertiaPanzoom.getScale();
397
+ if (target === viewerState.scale.value && Math.abs(visual - 1) < 5e-3) return;
398
+ if (!viewportEl || !anchor || !anchor.isConnected) {
399
+ viewerState.fitMode.value = "custom";
400
+ viewerState.scale.value = target;
401
+ return;
402
+ }
403
+ const vpRect = viewportEl.getBoundingClientRect();
404
+ const fx = vpRect.width / 2;
405
+ const fy = vpRect.height / 2;
406
+ const aRect = anchor.getBoundingClientRect();
407
+ const qx = (vpRect.left + fx - aRect.left) / aRect.width;
408
+ const qy = (vpRect.top + fy - aRect.top) / aRect.height;
409
+ suppressScaleReset = target !== viewerState.scale.value;
410
+ viewerState.fitMode.value = "custom";
411
+ viewerState.scale.value = target;
412
+ await nextTick();
413
+ const t = inertiaPanzoom.getTransform();
414
+ const vp2 = viewportEl.getBoundingClientRect();
415
+ const a2 = anchor.getBoundingClientRect();
416
+ const layoutLeft = (a2.left - vp2.left - t.tx) / t.scale;
417
+ const layoutTop = (a2.top - vp2.top - t.ty) / t.scale;
418
+ const layoutW = a2.width / t.scale;
419
+ const layoutH = a2.height / t.scale;
420
+ inertiaPanzoom.setTransform({
421
+ scale: 1,
422
+ tx: fx - (layoutLeft + qx * layoutW),
423
+ ty: fy - (layoutTop + qy * layoutH)
424
+ });
425
+ viewWindows.value = {};
426
+ computeViewWindows();
427
+ }
428
+ setApplyScaleFn((value) => {
429
+ void commitZoom(value);
430
+ });
431
+ watch(
432
+ () => viewerState.scale.value,
433
+ (s) => inertiaPanzoom.setScaleLimits(
434
+ Math.min(1, MIN_ZOOM / s),
435
+ Math.max(1, MAX_ZOOM / s)
436
+ ),
437
+ { immediate: true }
438
+ );
439
+ inertiaPanzoom.onZoomEnd((visual) => {
440
+ if (Math.abs(visual - 1) < 5e-3) return;
441
+ void commitZoom(viewerState.scale.value * visual);
442
+ });
443
+ const viewWindows = shallowRef({});
444
+ const VIEW_WINDOW_MARGIN = 0.25;
445
+ function computeViewWindows() {
446
+ const viewportEl = zoomWrapper.value?.parentElement;
447
+ if (!viewportEl) return;
448
+ const vpRect = viewportEl.getBoundingClientRect();
449
+ if (vpRect.width === 0 || vpRect.height === 0) return;
450
+ const prev = viewWindows.value;
451
+ const next = {};
452
+ let changed = false;
453
+ for (const meta of pageMetas.value) {
454
+ const el = virtualization.getPageElement(meta.pageNumber);
455
+ if (!el) continue;
456
+ const r = el.getBoundingClientRect();
457
+ if (r.width === 0 || el.offsetWidth === 0) continue;
458
+ const k = r.width / el.offsetWidth;
459
+ const vx = (Math.max(vpRect.left, r.left) - r.left) / k;
460
+ const vy = (Math.max(vpRect.top, r.top) - r.top) / k;
461
+ const vr = (Math.min(vpRect.right, r.right) - r.left) / k;
462
+ const vb = (Math.min(vpRect.bottom, r.bottom) - r.top) / k;
463
+ if (vr <= vx || vb <= vy) continue;
464
+ const old = prev[meta.pageNumber];
465
+ if (old && vx >= old.x && vy >= old.y && vr <= old.x + old.width && vb <= old.y + old.height) {
466
+ next[meta.pageNumber] = old;
467
+ continue;
468
+ }
469
+ const mx = vpRect.width / k * VIEW_WINDOW_MARGIN;
470
+ const my = vpRect.height / k * VIEW_WINDOW_MARGIN;
471
+ next[meta.pageNumber] = {
472
+ x: vx - mx,
473
+ y: vy - my,
474
+ width: vr - vx + 2 * mx,
475
+ height: vb - vy + 2 * my
476
+ };
477
+ changed = true;
478
+ }
479
+ if (changed || Object.keys(next).length !== Object.keys(prev).length) {
480
+ viewWindows.value = next;
481
+ }
482
+ }
483
+ let viewWindowTimer = 0;
484
+ let lastViewWindowUpdate = 0;
485
+ inertiaPanzoom.onChange(() => {
486
+ const now = performance.now();
487
+ const wait = Math.max(0, 250 - (now - lastViewWindowUpdate));
488
+ clearTimeout(viewWindowTimer);
489
+ viewWindowTimer = window.setTimeout(() => {
490
+ lastViewWindowUpdate = performance.now();
491
+ computeViewWindows();
492
+ }, wait);
493
+ });
494
+ function pageViewWindow(pageNumber) {
495
+ if (pageSettings.rotation.value !== 0) return null;
496
+ return viewWindows.value[pageNumber] ?? null;
497
+ }
331
498
  function pageSlotStyle(meta) {
332
499
  const s = pageScale.value;
333
500
  if (isRotatedSideways.value) {
@@ -424,26 +591,36 @@ async function loadDocument() {
424
591
  }
425
592
  const source = props.source;
426
593
  const baseParams = source instanceof Uint8Array || typeof source === "string" ? { url: typeof source === "string" ? source : void 0, data: source instanceof Uint8Array ? source : void 0 } : { ...source };
594
+ if (baseParams.data instanceof Uint8Array) {
595
+ baseParams.data = baseParams.data.slice();
596
+ } else if (baseParams.data instanceof ArrayBuffer) {
597
+ baseParams.data = baseParams.data.slice(0);
598
+ }
427
599
  const docParams = {
428
600
  ...baseParams,
429
601
  standardFontDataUrl: "/_kviewer/standard_fonts/",
430
602
  cMapUrl: "/_kviewer/cmaps/",
431
603
  cMapPacked: true,
432
- useSystemFonts: true
604
+ useSystemFonts: true,
605
+ wasmUrl: "/_kviewer/wasm/",
606
+ iccUrl: "/_kviewer/iccs/"
433
607
  };
434
608
  const loadingTask = pdfjs.getDocument(docParams);
609
+ destroyActiveDoc = () => loadingTask.destroy();
435
610
  const pdfDoc = await loadingTask.promise;
436
611
  doc.value = pdfDoc;
437
612
  viewerState.doc.value = pdfDoc;
438
613
  viewerState.totalPages.value = pdfDoc.numPages;
439
614
  proxyCache.setDocument(pdfDoc);
440
615
  pdfDoc.getData().then(async (bytes) => {
441
- const [styles, captions] = await Promise.all([
616
+ const [styles, captions, signedSigs] = await Promise.all([
442
617
  extractCheckboxStyles(bytes),
443
- extractButtonCaptions(bytes)
618
+ extractButtonCaptions(bytes),
619
+ extractSignedSignatureNames(bytes)
444
620
  ]);
445
621
  if (styles.size > 0) formFieldsState.applyCheckboxStyles(styles);
446
622
  if (captions.size > 0) formFieldsState.applyButtonCaptions(captions);
623
+ if (signedSigs.size > 0) formFieldsState.applySignedSignatures(signedSigs);
447
624
  }).catch((err) => console.warn("[KViewer] Failed to read MK data from source PDF:", err));
448
625
  if (!scrollContainer.value) {
449
626
  await nextTick();
@@ -794,10 +971,18 @@ onMounted(() => {
794
971
  { immediate: true }
795
972
  );
796
973
  watch(() => viewerState.scale.value, async () => {
974
+ if (suppressScaleReset) {
975
+ suppressScaleReset = false;
976
+ return;
977
+ }
797
978
  await nextTick();
798
979
  inertiaPanzoom.reset();
980
+ viewWindows.value = {};
981
+ computeViewWindows();
982
+ });
983
+ watch(() => viewerState.currentPage.value, () => {
984
+ if (isSinglePageMode.value) inertiaPanzoom.reset();
799
985
  });
800
- watch(() => viewerState.currentPage.value, () => inertiaPanzoom.reset());
801
986
  }
802
987
  if (isIPad()) {
803
988
  viewerState.setStylusMode(true);
@@ -818,7 +1003,7 @@ watch(
818
1003
  searchIndex.destroy();
819
1004
  virtualization.destroy();
820
1005
  proxyCache.clear();
821
- doc.value?.destroy();
1006
+ void destroyActiveDoc?.();
822
1007
  loadDocument();
823
1008
  }
824
1009
  );
@@ -850,6 +1035,7 @@ watch(
850
1035
  );
851
1036
  onBeforeUnmount(() => {
852
1037
  window.removeEventListener("keydown", onGlobalKeydown);
1038
+ clearTimeout(viewWindowTimer);
853
1039
  scrollContainer.value?.removeEventListener("wheel", onWheel);
854
1040
  inertiaPanzoom.detach();
855
1041
  resizeObserver?.disconnect();
@@ -864,7 +1050,8 @@ onBeforeUnmount(() => {
864
1050
  virtualization.destroy();
865
1051
  proxyCache.clear();
866
1052
  painter?.destroy();
867
- doc.value?.destroy();
1053
+ void destroyActiveDoc?.();
1054
+ destroyActiveDoc = null;
868
1055
  });
869
1056
  defineExpose({
870
1057
  getAnnotations: () => painter?.getData() ?? [],
@@ -892,7 +1079,15 @@ defineExpose({
892
1079
  const next = !viewerState.formEditMode.value;
893
1080
  viewerState.setFormEditMode(next);
894
1081
  return next;
895
- }
1082
+ },
1083
+ /** True once the user has viewed every page — each page has entered the
1084
+ * viewport at least once. Use to gate a "must read all pages" sign action.
1085
+ * See also the `all-pages-read` event for a push-style signal. */
1086
+ allPagesRead: () => virtualization.allPagesRead.value,
1087
+ /** Page numbers viewed so far, ascending. */
1088
+ getViewedPages: () => [...virtualization.viewedPages.value].sort((a, b) => a - b),
1089
+ /** Clear viewed-page tracking, e.g. to restart a signing session. */
1090
+ resetViewedPages: () => virtualization.resetViewedPages()
896
1091
  });
897
1092
  </script>
898
1093
 
@@ -2,6 +2,9 @@ import { type ViewMode } from '../composables/useViewerState.js';
2
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
  import type { ViewerMenuItem } from '../menu-items.js';
5
+ import type { ViewerToolEntry } from '../toolbar-tools.js';
6
+ import { type KviewerMessageOverrides } from '../composables/useKviewerI18n.js';
7
+ import { type KviewerLocale } from '../i18n/messages.js';
5
8
  type __VLS_Props = {
6
9
  source: string | Uint8Array | object;
7
10
  textLayer?: boolean;
@@ -45,6 +48,19 @@ type __VLS_Props = {
45
48
  * toggles or actions without replacing the entire header. Ignored
46
49
  * when the host supplies a custom `#header` slot. */
47
50
  menuItems?: ViewerMenuItem[];
51
+ /** Configures which toolbar tools render and in what order. Entries are
52
+ * built-in tool IDs (strings) or `{ type: 'slot', ... }` objects that
53
+ * inject a custom button via a `#tool-<name>` slot. When omitted, the
54
+ * default toolbar is rendered unchanged. Ignored when the host supplies
55
+ * a custom `#header` slot. See {@link ViewerToolEntry}. */
56
+ tools?: ViewerToolEntry[];
57
+ /** UI language for built-in viewer strings (toolbar, search, modals,
58
+ * form sidebar). Defaults to the `kviewer.locale` module option, then
59
+ * 'en'. Hosts with their own i18n just bind their active locale. */
60
+ locale?: KviewerLocale;
61
+ /** Per-string overrides for built-in messages — wins over the locale
62
+ * catalog. Useful for app-specific wording without forking locales. */
63
+ messages?: KviewerMessageOverrides;
48
64
  };
49
65
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
50
66
  type ImportMode = 'replace' | 'merge';
@@ -55,11 +71,15 @@ declare function importAnnotations(annotations: IAnnotationStore[], options?: {
55
71
  skipped: number;
56
72
  }>;
57
73
  declare function getKonvaCanvasState(): Record<number, string>;
58
- declare var __VLS_1: {}, __VLS_51: {};
74
+ declare var __VLS_1: {}, __VLS_11: string, __VLS_12: {
75
+ state: import("../composables/useViewerState.js").ViewerState;
76
+ }, __VLS_62: {};
59
77
  type __VLS_Slots = {} & {
78
+ [K in NonNullable<typeof __VLS_11>]?: (props: typeof __VLS_12) => any;
79
+ } & {
60
80
  header?: (props: typeof __VLS_1) => any;
61
81
  } & {
62
- footer?: (props: typeof __VLS_51) => any;
82
+ footer?: (props: typeof __VLS_62) => any;
63
83
  };
64
84
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
65
85
  getAnnotations: () => IAnnotationStore[];
@@ -84,12 +104,24 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
84
104
  setFormEditMode: (enabled: boolean) => void;
85
105
  /** Flip form-edit mode. Returns the new state. */
86
106
  toggleFormEditMode: () => boolean;
107
+ /** True once the user has viewed every page — each page has entered the
108
+ * viewport at least once. Use to gate a "must read all pages" sign action.
109
+ * See also the `all-pages-read` event for a push-style signal. */
110
+ allPagesRead: () => boolean;
111
+ /** Page numbers viewed so far, ascending. */
112
+ getViewedPages: () => number[];
113
+ /** Clear viewed-page tracking, e.g. to restart a signing session. */
114
+ resetViewedPages: () => void;
87
115
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
116
+ "all-pages-read": () => any;
88
117
  "update:formEditMode": (value: boolean) => any;
89
118
  "update:activeRoleId": (value: string | null) => any;
119
+ "update:viewedPages": (pages: number[]) => any;
90
120
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
121
+ "onAll-pages-read"?: (() => any) | undefined;
91
122
  "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
92
123
  "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
124
+ "onUpdate:viewedPages"?: ((pages: number[]) => any) | undefined;
93
125
  }>, {
94
126
  userName: string;
95
127
  freehandGroupingDelay: number;
@@ -97,6 +129,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
97
129
  readonly: boolean;
98
130
  active: boolean;
99
131
  menuItems: ViewerMenuItem[];
132
+ tools: ViewerToolEntry[];
100
133
  scripting: boolean;
101
134
  stamps: StampDefinition[];
102
135
  signatureHandlers: SignatureHandlers;
@@ -105,6 +138,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
105
138
  formEditMode: boolean;
106
139
  roleColors: Record<string, string>;
107
140
  activeRoleId: string | null;
141
+ locale: KviewerLocale;
142
+ messages: KviewerMessageOverrides;
108
143
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
109
144
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
110
145
  declare const _default: typeof __VLS_export;
@@ -1,9 +1,29 @@
1
1
  import type { ViewerMenuItem } from '../menu-items.js';
2
+ import { type ViewerToolEntry } from '../toolbar-tools.js';
2
3
  type __VLS_Props = {
3
4
  /** Extra items appended to the burger menu after the built-ins.
4
5
  * See {@link ViewerMenuItem}. Pure forwarding from `<KViewer>`. */
5
6
  menuItems?: ViewerMenuItem[];
7
+ /** Configures which toolbar tools render and in what order. When
8
+ * omitted, the default toolbar is rendered. See {@link ViewerToolEntry}. */
9
+ tools?: ViewerToolEntry[];
6
10
  };
7
- 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>;
11
+ declare var __VLS_109: string, __VLS_110: {
12
+ state: import("../composables/useViewerState.js").ViewerState;
13
+ }, __VLS_154: string, __VLS_155: {
14
+ state: import("../composables/useViewerState.js").ViewerState;
15
+ };
16
+ type __VLS_Slots = {} & {
17
+ [K in NonNullable<typeof __VLS_109>]?: (props: typeof __VLS_110) => any;
18
+ } & {
19
+ [K in NonNullable<typeof __VLS_154>]?: (props: typeof __VLS_155) => any;
20
+ };
21
+ declare const __VLS_base: 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>;
22
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
8
23
  declare const _default: typeof __VLS_export;
9
24
  export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };