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
@@ -16,6 +16,15 @@
16
16
  ref="canvasRef"
17
17
  :style="{ display: 'block', width: cssWidth + 'px', height: cssHeight + 'px' }"
18
18
  />
19
+ <!-- Crisp viewport-window raster: when the full page would blow the
20
+ canvas budget at high zoom, the base canvas above is only a
21
+ low-res fallback and this canvas renders just the visible region
22
+ (plus margin) at full resolution. -->
23
+ <canvas
24
+ v-show="windowCanvasVisible"
25
+ ref="windowCanvasRef"
26
+ :style="windowCanvasStyle"
27
+ />
19
28
  <div
20
29
  ref="textLayerRef"
21
30
  class="kviewer-text-layer textLayer"
@@ -33,6 +42,16 @@
33
42
  :pointer-events="formFieldPointerEvents"
34
43
  :z-index="formFieldZIndex"
35
44
  />
45
+ <!-- transform: translateZ(0) isolates the Konva canvases in their own
46
+ compositing layer: per-frame canvas updates during annotation
47
+ drags then only re-upload the (budget-capped) Konva texture
48
+ instead of triggering an iPadOS re-rasterization of the subtree
49
+ containing the much larger PDF canvases. -->
50
+ <!-- overflow: hidden — between a zoom commit and the async Konva
51
+ re-init the old (differently sized) stage briefly overflows this
52
+ div; unclipped it inflates the zoom wrapper's scrollHeight and
53
+ the panzoom clamps against phantom bounds, leaving the page
54
+ resting out of position. -->
36
55
  <div
37
56
  ref="annotationRef"
38
57
  class="kviewer-annotation-layer"
@@ -43,7 +62,9 @@
43
62
  width: cssWidth + 'px',
44
63
  height: cssHeight + 'px',
45
64
  zIndex: 3,
46
- pointerEvents: annotationPointerEvents
65
+ pointerEvents: annotationPointerEvents,
66
+ transform: 'translateZ(0)',
67
+ overflow: 'hidden'
47
68
  }"
48
69
  />
49
70
  </div>
@@ -58,21 +79,35 @@ import { usePageProxyCache } from "../composables/usePageProxyCache";
58
79
  import { useFormFields } from "../composables/useFormFields";
59
80
  import { useShapeDetection } from "../composables/useShapeDetection";
60
81
  import { AnnotationType } from "../annotation/engine/types";
82
+ import { isIPad } from "../annotation/engine/input-device";
83
+ import { extractSignedSignatureAppearancesForPage } from "../annotation/pdf-import/extract_stamp_appearance";
84
+ import { pageTransformFromProxy } from "../annotation/pdf-import/transform";
85
+ import {
86
+ clampRenderScale,
87
+ DEFAULT_MAX_CANVAS_PIXELS,
88
+ IOS_MAX_CANVAS_PIXELS
89
+ } from "../composables/canvas-limits";
61
90
  import FormFieldLayer from "./FormFieldLayer.vue";
62
91
  const PDFJS_ANNOTATION_MODE_DISABLE = 0;
63
92
  const props = defineProps({
64
93
  pageNumber: { type: Number, required: true },
65
94
  pageMeta: { type: Object, required: true },
66
95
  scale: { type: Number, required: true },
67
- renderQuality: { type: Number, required: false, default: 1 }
96
+ renderQuality: { type: Number, required: false, default: 1 },
97
+ viewWindow: { type: [Object, null], required: false, default: null }
68
98
  });
69
99
  const canvasRef = ref(null);
100
+ const windowCanvasRef = ref(null);
101
+ const windowCanvasVisible = ref(false);
102
+ const windowCanvasStyle = ref({});
70
103
  const textLayerRef = ref(null);
71
104
  const annotationRef = ref(null);
72
105
  const runtimeConfig = useRuntimeConfig();
73
106
  const minRenderPixelRatio = Number(
74
107
  runtimeConfig.public.kviewer?.minRenderPixelRatio ?? 2
75
108
  );
109
+ const isAppleMobile = typeof navigator !== "undefined" && (isIPad() || /iPhone|iPod/.test(navigator.userAgent));
110
+ const maxCanvasPixels = isAppleMobile ? IOS_MAX_CANVAS_PIXELS : DEFAULT_MAX_CANVAS_PIXELS;
76
111
  const state = useViewerState();
77
112
  const search = useViewerSearch();
78
113
  const proxyCache = usePageProxyCache();
@@ -80,6 +115,11 @@ const formFields = useFormFields();
80
115
  const shapeDetection = useShapeDetection();
81
116
  const pageProxy = shallowRef(null);
82
117
  let currentRenderTask = null;
118
+ let windowRenderTask = null;
119
+ let windowed = false;
120
+ let renderedWindowKey = null;
121
+ const FALLBACK_MAX_CANVAS_PIXELS = 4e6;
122
+ const WINDOW_MAX_CANVAS_PIXELS = 8e6;
83
123
  let textLayer = null;
84
124
  const baseWidth = props.pageMeta.width;
85
125
  const baseHeight = props.pageMeta.height;
@@ -109,8 +149,8 @@ onMounted(async () => {
109
149
  await renderTextLayer();
110
150
  initKonva();
111
151
  if (pageProxy.value) {
112
- const rawAnnotations = await pageProxy.value.getAnnotations();
113
- formFields.parsePageFields(props.pageNumber, rawAnnotations);
152
+ rawPageAnnotations = await pageProxy.value.getAnnotations();
153
+ formFields.parsePageFields(props.pageNumber, rawPageAnnotations);
114
154
  }
115
155
  if (state.shapeDetection.value && pageProxy.value) {
116
156
  const viewport = pageProxy.value.getViewport({ scale: props.scale });
@@ -129,6 +169,27 @@ onMounted(async () => {
129
169
  }
130
170
  }
131
171
  });
172
+ let rawPageAnnotations = null;
173
+ const attemptedSignatureSeedIds = /* @__PURE__ */ new Set();
174
+ const pendingSignedSignatureKey = computed(() => formFields.getFieldsForPage(props.pageNumber).filter((def) => def.fieldType === "signature" && def.signedInSource && formFields.getFieldValue(def.id)?.value === "").map((def) => def.id).join(","));
175
+ watch(pendingSignedSignatureKey, (key) => {
176
+ if (key !== "") void seedSignedSignatureAppearances();
177
+ });
178
+ async function seedSignedSignatureAppearances() {
179
+ if (!pageProxy.value || !rawPageAnnotations) return;
180
+ const pending = formFields.getFieldsForPage(props.pageNumber).filter((def) => def.fieldType === "signature" && def.signedInSource && formFields.getFieldValue(def.id)?.value === "" && !attemptedSignatureSeedIds.has(def.id));
181
+ if (pending.length === 0) return;
182
+ for (const def of pending) attemptedSignatureSeedIds.add(def.id);
183
+ const appearances = await extractSignedSignatureAppearancesForPage(
184
+ pageProxy.value,
185
+ pageTransformFromProxy(pageProxy.value),
186
+ rawPageAnnotations
187
+ );
188
+ for (const def of pending) {
189
+ const appearance = appearances.get(def.id);
190
+ if (appearance) formFields.setFieldValue(def.id, appearance.dataUrl);
191
+ }
192
+ }
132
193
  watch(
133
194
  () => state.shapeDetection.value,
134
195
  async (enabled) => {
@@ -151,10 +212,22 @@ watch(
151
212
  );
152
213
  watch(
153
214
  () => props.scale,
154
- async () => {
215
+ async (newScale, oldScale) => {
155
216
  if (!pageProxy.value) return;
217
+ if (windowCanvasVisible.value && oldScale && newScale !== oldScale) {
218
+ const r = newScale / oldScale;
219
+ const s = windowCanvasStyle.value;
220
+ windowCanvasStyle.value = {
221
+ ...s,
222
+ left: Number.parseFloat(s.left ?? "0") * r + "px",
223
+ top: Number.parseFloat(s.top ?? "0") * r + "px",
224
+ width: Number.parseFloat(s.width ?? "0") * r + "px",
225
+ height: Number.parseFloat(s.height ?? "0") * r + "px"
226
+ };
227
+ renderedWindowKey = null;
228
+ }
156
229
  shapeDetection.clearShapeCache(props.pageNumber);
157
- await renderCanvas();
230
+ await renderCanvas({ doubleBuffer: true });
158
231
  updateTextLayer();
159
232
  if (state.painter.value) {
160
233
  state.painter.value.destroyCanvasForPage(props.pageNumber);
@@ -183,9 +256,16 @@ async function renderCanvas(opts) {
183
256
  }
184
257
  const dpr = Math.max(window.devicePixelRatio || 1, minRenderPixelRatio);
185
258
  const quality = props.renderQuality ?? 1;
186
- const viewport = proxy.getViewport({
187
- scale: props.scale * quality * dpr
188
- });
259
+ const desiredScale = props.scale * quality * dpr;
260
+ const fullScale = clampRenderScale(desiredScale, baseWidth, baseHeight, maxCanvasPixels);
261
+ windowed = fullScale < desiredScale * 0.999;
262
+ const renderScale = windowed ? clampRenderScale(
263
+ desiredScale,
264
+ baseWidth,
265
+ baseHeight,
266
+ Math.min(FALLBACK_MAX_CANVAS_PIXELS, maxCanvasPixels)
267
+ ) : fullScale;
268
+ const viewport = proxy.getViewport({ scale: renderScale });
189
269
  if (opts?.doubleBuffer) {
190
270
  const tmp = document.createElement("canvas");
191
271
  tmp.width = viewport.width;
@@ -225,7 +305,84 @@ async function renderCanvas(opts) {
225
305
  currentRenderTask = null;
226
306
  }
227
307
  }
308
+ if (windowed) {
309
+ void renderWindowCanvas();
310
+ } else {
311
+ hideWindowCanvas();
312
+ }
313
+ }
314
+ function hideWindowCanvas() {
315
+ windowCanvasVisible.value = false;
316
+ renderedWindowKey = null;
317
+ const c = windowCanvasRef.value;
318
+ if (c && c.width > 0) {
319
+ c.width = 0;
320
+ c.height = 0;
321
+ }
228
322
  }
323
+ async function renderWindowCanvas() {
324
+ const proxy = pageProxy.value;
325
+ const canvas = windowCanvasRef.value;
326
+ const win = props.viewWindow;
327
+ if (!proxy || !canvas || !win || !windowed) return;
328
+ const x = Math.max(0, Math.min(win.x, cssWidth.value));
329
+ const y = Math.max(0, Math.min(win.y, cssHeight.value));
330
+ const w = Math.min(win.x + win.width, cssWidth.value) - x;
331
+ const h = Math.min(win.y + win.height, cssHeight.value) - y;
332
+ if (w <= 0 || h <= 0) return;
333
+ const key = [x, y, w, h, props.scale].map((n) => Math.round(n)).join(",");
334
+ if (key === renderedWindowKey && windowCanvasVisible.value) return;
335
+ const dpr = window.devicePixelRatio || 1;
336
+ const renderScale = clampRenderScale(
337
+ props.scale * dpr,
338
+ w / props.scale,
339
+ h / props.scale,
340
+ Math.min(WINDOW_MAX_CANVAS_PIXELS, maxCanvasPixels)
341
+ );
342
+ const k = renderScale / props.scale;
343
+ const viewport = proxy.getViewport({ scale: renderScale });
344
+ windowRenderTask?.cancel();
345
+ await currentRenderTask?.promise.catch(() => {
346
+ });
347
+ const tmp = document.createElement("canvas");
348
+ tmp.width = Math.floor(w * k);
349
+ tmp.height = Math.floor(h * k);
350
+ try {
351
+ windowRenderTask = proxy.render({
352
+ canvasContext: tmp.getContext("2d"),
353
+ viewport,
354
+ transform: [1, 0, 0, 1, -x * k, -y * k],
355
+ annotationMode: PDFJS_ANNOTATION_MODE_DISABLE
356
+ });
357
+ await windowRenderTask.promise;
358
+ canvas.width = tmp.width;
359
+ canvas.height = tmp.height;
360
+ canvas.getContext("2d").drawImage(tmp, 0, 0);
361
+ windowCanvasStyle.value = {
362
+ position: "absolute",
363
+ left: x + "px",
364
+ top: y + "px",
365
+ width: w + "px",
366
+ height: h + "px"
367
+ };
368
+ windowCanvasVisible.value = true;
369
+ renderedWindowKey = key;
370
+ } catch (e) {
371
+ if (e instanceof Error && e.message?.includes("Rendering cancelled")) return;
372
+ console.warn("[KViewer] Window render failed:", e);
373
+ } finally {
374
+ windowRenderTask = null;
375
+ tmp.width = 0;
376
+ tmp.height = 0;
377
+ }
378
+ }
379
+ watch(
380
+ () => props.viewWindow,
381
+ (win) => {
382
+ if (windowed) void renderWindowCanvas();
383
+ state.painter.value?.setStageWindow(props.pageNumber, win ?? null);
384
+ }
385
+ );
229
386
  function initKonva() {
230
387
  if (!annotationRef.value || !state.painter.value) return;
231
388
  state.painter.value.initCanvasForPage(
@@ -235,6 +392,9 @@ function initKonva() {
235
392
  baseHeight,
236
393
  props.scale
237
394
  );
395
+ if (props.viewWindow) {
396
+ state.painter.value.setStageWindow(props.pageNumber, props.viewWindow);
397
+ }
238
398
  }
239
399
  async function renderTextLayer() {
240
400
  const container = textLayerRef.value;
@@ -279,6 +439,7 @@ function updateTextLayer() {
279
439
  }
280
440
  onBeforeUnmount(() => {
281
441
  currentRenderTask?.cancel();
442
+ windowRenderTask?.cancel();
282
443
  textLayer?.cancel();
283
444
  textLayer = null;
284
445
  search.unregisterPageLayer(props.pageNumber);
@@ -6,9 +6,25 @@ type __VLS_Props = {
6
6
  /** Canvas internal resolution multiplier (CSS size unchanged).
7
7
  * Used by panzoom to sharpen the PDF when visually zoomed in. */
8
8
  renderQuality?: number;
9
+ /** Visible region of this page in page-CSS coordinates (already
10
+ * includes margin), supplied by the viewer's panzoom. Drives the
11
+ * viewport-window raster at high zoom. Null = unknown/inapplicable
12
+ * (e.g. rotated pages), full-page rendering only. */
13
+ viewWindow?: {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ } | null;
9
19
  };
10
20
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
21
  renderQuality: number;
22
+ viewWindow: {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ } | null;
12
28
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
29
  declare const _default: typeof __VLS_export;
14
30
  export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <div v-if="enabled" class="perf-hud">
3
+ <div>fps {{ fps }} | p95 {{ p95 }}ms | max {{ maxGap }}ms</div>
4
+ <div>evt/s {{ evtRate }} | konva {{ drawPerSec }}ms/s | long {{ longSupported ? longMs + "ms/s" : "n/a" }}</div>
5
+ <div>{{ canvasInfo }}</div>
6
+ <button class="perf-hud-btn" @pointerdown.stop @click="noDraw = !noDraw">
7
+ konva draw: {{ noDraw ? "OFF" : "on" }}
8
+ </button>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { ref, onMounted, onBeforeUnmount } from "vue";
14
+ import Konva from "konva";
15
+ const enabled = ref(false);
16
+ const fps = ref(0);
17
+ const p95 = ref(0);
18
+ const maxGap = ref(0);
19
+ const evtRate = ref(0);
20
+ const drawPerSec = ref(0);
21
+ const longMs = ref(0);
22
+ const longSupported = ref(false);
23
+ const canvasInfo = ref("");
24
+ const noDraw = ref(false);
25
+ let raf = 0;
26
+ let interval = 0;
27
+ let last = 0;
28
+ let gaps = [];
29
+ let evtCount = 0;
30
+ let drawMs = 0;
31
+ let longTaskMs = 0;
32
+ let po = null;
33
+ let origDraw = null;
34
+ function onAnyMove() {
35
+ evtCount++;
36
+ }
37
+ onMounted(() => {
38
+ enabled.value = new URLSearchParams(window.location.search).has("perf");
39
+ if (!enabled.value) return;
40
+ origDraw = Konva.Layer.prototype.drawScene;
41
+ Konva.Layer.prototype.drawScene = function(...args) {
42
+ if (noDraw.value) return this;
43
+ const t0 = performance.now();
44
+ const r = origDraw.apply(this, args);
45
+ drawMs += performance.now() - t0;
46
+ return r;
47
+ };
48
+ window.addEventListener("pointermove", onAnyMove, { capture: true, passive: true });
49
+ window.addEventListener("touchmove", onAnyMove, { capture: true, passive: true });
50
+ try {
51
+ po = new PerformanceObserver((list) => {
52
+ for (const e of list.getEntries()) longTaskMs += e.duration;
53
+ });
54
+ po.observe({ type: "longtask" });
55
+ longSupported.value = true;
56
+ } catch {
57
+ longSupported.value = false;
58
+ }
59
+ const tick = (t) => {
60
+ if (last) gaps.push(t - last);
61
+ last = t;
62
+ raf = requestAnimationFrame(tick);
63
+ };
64
+ raf = requestAnimationFrame(tick);
65
+ interval = window.setInterval(() => {
66
+ const sorted = [...gaps].sort((a, b) => a - b);
67
+ const total = gaps.reduce((s, g) => s + g, 0);
68
+ fps.value = total > 0 ? Math.round(gaps.length / total * 1e3) : 0;
69
+ p95.value = Math.round(sorted[Math.floor(sorted.length * 0.95)] ?? 0);
70
+ maxGap.value = Math.round(sorted[sorted.length - 1] ?? 0);
71
+ evtRate.value = evtCount;
72
+ drawPerSec.value = Math.round(drawMs);
73
+ longMs.value = Math.round(longTaskMs);
74
+ gaps = [];
75
+ evtCount = 0;
76
+ drawMs = 0;
77
+ longTaskMs = 0;
78
+ const pageEl = document.querySelector(".kviewer-page");
79
+ if (pageEl) {
80
+ canvasInfo.value = [...pageEl.querySelectorAll(":scope > canvas, .konvajs-content canvas")].map((c) => `${c.width}x${c.height}`).join(" | ");
81
+ }
82
+ }, 1e3);
83
+ });
84
+ onBeforeUnmount(() => {
85
+ if (!enabled.value) return;
86
+ cancelAnimationFrame(raf);
87
+ clearInterval(interval);
88
+ window.removeEventListener("pointermove", onAnyMove, { capture: true });
89
+ window.removeEventListener("touchmove", onAnyMove, { capture: true });
90
+ po?.disconnect();
91
+ if (origDraw) Konva.Layer.prototype.drawScene = origDraw;
92
+ });
93
+ </script>
94
+
95
+ <style scoped>
96
+ .perf-hud{background:rgba(0,0,0,.8);border-radius:6px;color:#4ade80;font:11px/1.4 ui-monospace,monospace;left:50%;padding:4px 8px;pointer-events:none;position:fixed;text-align:center;top:4px;transform:translateX(-50%);white-space:nowrap;z-index:99999}.perf-hud-btn{background:#333;border:1px solid #666;border-radius:4px;color:#fff;font:inherit;margin-top:2px;padding:2px 8px;pointer-events:auto}
97
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -3,7 +3,8 @@
3
3
  v-if="tool"
4
4
  class="relative flex items-center justify-center w-8 h-8 rounded-md transition-colors cursor-pointer"
5
5
  :class="active ? 'bg-default text-highlighted' : 'text-muted hover:bg-default/50 hover:text-highlighted'"
6
- :title="tool.name"
6
+ :title="toolLabel(tool.name)"
7
+ :data-testid="`tool-${tool.name}`"
7
8
  @click="$emit('click')"
8
9
  >
9
10
  <UIcon :name="tool.icon" class="text-lg" />
@@ -17,6 +18,8 @@
17
18
  </template>
18
19
 
19
20
  <script setup>
21
+ import { useKviewerI18n } from "../composables/useKviewerI18n";
22
+ const { toolLabel } = useKviewerI18n();
20
23
  defineProps({
21
24
  tool: { type: null, required: true },
22
25
  active: { type: Boolean, required: true },
@@ -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;