kviewer 0.0.8 → 0.0.10

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 (34) hide show
  1. package/dist/module.d.mts +1 -1
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/annotation/engine/editor/editor.d.ts +11 -5
  4. package/dist/runtime/annotation/engine/editor/editor.js +27 -11
  5. package/dist/runtime/annotation/engine/editor/selector.d.ts +20 -1
  6. package/dist/runtime/annotation/engine/editor/selector.js +132 -33
  7. package/dist/runtime/annotation/engine/painter.d.ts +4 -1
  8. package/dist/runtime/annotation/engine/painter.js +21 -5
  9. package/dist/runtime/annotation/engine/tools/freehand.js +2 -2
  10. package/dist/runtime/components/PdfPage.d.vue.ts +6 -1
  11. package/dist/runtime/components/PdfPage.vue +51 -18
  12. package/dist/runtime/components/PdfPage.vue.d.ts +6 -1
  13. package/dist/runtime/components/ToolButton.d.vue.ts +4 -2
  14. package/dist/runtime/components/ToolButton.vue.d.ts +4 -2
  15. package/dist/runtime/components/Viewer.d.vue.ts +3 -0
  16. package/dist/runtime/components/Viewer.vue +225 -165
  17. package/dist/runtime/components/Viewer.vue.d.ts +3 -0
  18. package/dist/runtime/components/form-fields/FormSignatureField.vue +23 -1
  19. package/dist/runtime/components/modals/FreeTextModal.vue +27 -3
  20. package/dist/runtime/components/tools/ActionTools.vue +4 -5
  21. package/dist/runtime/components/tools/DrawingTools.vue +1 -1
  22. package/dist/runtime/composables/useAnnotationEngine.d.ts +2 -0
  23. package/dist/runtime/composables/useAnnotationEngine.js +5 -1
  24. package/dist/runtime/composables/useAnnotationHistory.d.ts +1 -0
  25. package/dist/runtime/composables/useAnnotationHistory.js +10 -3
  26. package/dist/runtime/composables/useInertiaPanzoom.d.ts +40 -0
  27. package/dist/runtime/composables/useInertiaPanzoom.js +472 -0
  28. package/dist/runtime/composables/usePageVirtualization.d.ts +3 -0
  29. package/dist/runtime/composables/usePageVirtualization.js +23 -0
  30. package/dist/runtime/composables/useViewerSearch.d.ts +1 -0
  31. package/dist/runtime/composables/useViewerSearch.js +15 -5
  32. package/dist/runtime/public-types.d.ts +2 -0
  33. package/dist/types.d.mts +1 -1
  34. package/package.json +1 -1
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- export { ExportPdfOptions } from '../dist/runtime/public-types.js';
2
+ export { ExportPdfOptions, SignatureData, SignatureHandlers, ViewMode } from '../dist/runtime/public-types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.0.8",
4
+ "version": "0.0.10",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -10,6 +10,8 @@ export interface IEditorOptions {
10
10
  onChange: (id: string, updates: Partial<IAnnotationStore>) => void;
11
11
  /** When provided and returns true, only pen/mouse input can draw. */
12
12
  getStylusModeEnabled?: () => boolean;
13
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. */
14
+ freehandGroupingDelay?: number;
13
15
  }
14
16
  export interface IShapeGroup {
15
17
  id: string;
@@ -30,8 +32,9 @@ export declare abstract class Editor {
30
32
  shapeGroupStore: Map<string, IShapeGroup>;
31
33
  currentShapeGroup: IShapeGroup | null;
32
34
  protected getStylusModeEnabled?: () => boolean;
35
+ protected freehandGroupingDelay: number;
33
36
  static MinSize: number;
34
- constructor({ userName, konvaStage, pageNumber, annotation, onAdd, editorType, onChange, getStylusModeEnabled, }: IEditorOptions & {
37
+ constructor({ userName, konvaStage, pageNumber, annotation, onAdd, editorType, onChange, getStylusModeEnabled, freehandGroupingDelay, }: IEditorOptions & {
35
38
  editorType: AnnotationType;
36
39
  });
37
40
  private dispatchAddEvent;
@@ -61,9 +64,12 @@ export declare abstract class Editor {
61
64
  addSerializedGroupToLayer(konvaStage: Konva.Stage, konvaString: string): void;
62
65
  deleteGroup(id: string, konvaStage: Konva.Stage): void;
63
66
  updateStyle(annotationStore: IAnnotationStore, style: IAnnotationStyle): void;
64
- static Timer: {
65
- [pageNumber: number]: number;
66
- };
67
+ static Timer: Map<number, {
68
+ id: number;
69
+ callback: (pageNumber: number) => void;
70
+ }>;
67
71
  static TimerClear(pageNumber: number): void;
68
- static TimerStart(pageNumber: number, callback: (pageNumber: number) => void): void;
72
+ static TimerStart(pageNumber: number, callback: (pageNumber: number) => void, delay?: number): void;
73
+ /** Immediately execute and clear all pending timers (e.g. before export). */
74
+ static flushAllTimers(): void;
69
75
  }
@@ -14,6 +14,7 @@ export class Editor {
14
14
  shapeGroupStore = /* @__PURE__ */ new Map();
15
15
  currentShapeGroup;
16
16
  getStylusModeEnabled;
17
+ freehandGroupingDelay;
17
18
  static MinSize = 8;
18
19
  constructor({
19
20
  userName,
@@ -23,7 +24,8 @@ export class Editor {
23
24
  onAdd,
24
25
  editorType,
25
26
  onChange,
26
- getStylusModeEnabled
27
+ getStylusModeEnabled,
28
+ freehandGroupingDelay
27
29
  }) {
28
30
  this.userName = userName;
29
31
  this.id = `${pageNumber}_${editorType}`;
@@ -36,6 +38,7 @@ export class Editor {
36
38
  this.onChange = onChange || (() => {
37
39
  });
38
40
  this.getStylusModeEnabled = getStylusModeEnabled;
41
+ this.freehandGroupingDelay = freehandGroupingDelay ?? 1e3;
39
42
  this.disableEditMode();
40
43
  this.enableEditMode();
41
44
  }
@@ -228,18 +231,31 @@ export class Editor {
228
231
  updateStyle(annotationStore, style) {
229
232
  this.changeStyle(annotationStore, style);
230
233
  }
231
- static Timer = {};
234
+ static Timer = /* @__PURE__ */ new Map();
232
235
  static TimerClear(pageNumber) {
233
- const timer = Editor.Timer[pageNumber];
234
- if (timer) {
235
- window.clearTimeout(timer);
236
+ const entry = Editor.Timer.get(pageNumber);
237
+ if (entry) {
238
+ window.clearTimeout(entry.id);
239
+ Editor.Timer.delete(pageNumber);
236
240
  }
237
241
  }
238
- static TimerStart(pageNumber, callback) {
239
- Editor.Timer[pageNumber] = window.setTimeout(() => {
240
- if (typeof callback === "function") {
241
- callback(pageNumber);
242
- }
243
- }, 1e3);
242
+ static TimerStart(pageNumber, callback, delay = 1e3) {
243
+ Editor.Timer.set(pageNumber, {
244
+ callback,
245
+ id: window.setTimeout(() => {
246
+ Editor.Timer.delete(pageNumber);
247
+ if (typeof callback === "function") {
248
+ callback(pageNumber);
249
+ }
250
+ }, delay)
251
+ });
252
+ }
253
+ /** Immediately execute and clear all pending timers (e.g. before export). */
254
+ static flushAllTimers() {
255
+ for (const [pageNumber, entry] of Editor.Timer) {
256
+ window.clearTimeout(entry.id);
257
+ Editor.Timer.delete(pageNumber);
258
+ entry.callback(pageNumber);
259
+ }
244
260
  }
245
261
  }
@@ -13,6 +13,13 @@ export interface ISelectorOptions {
13
13
  onFreeTextDoubleClick?: (id: string) => void;
14
14
  /** When provided and returns true, only pen/mouse input can interact. */
15
15
  getStylusModeEnabled?: () => boolean;
16
+ /**
17
+ * Toggle external gesture handlers (e.g. panzoom) off while an annotation
18
+ * is being dragged/transformed. Without this, on mobile the panzoom
19
+ * single-finger pan runs in parallel with Konva's shape drag and both
20
+ * CSS transforms compete every frame, causing visible lag.
21
+ */
22
+ setExternalGesturesEnabled?: (enabled: boolean) => void;
16
23
  }
17
24
  export declare class Selector {
18
25
  readonly onSelected: (id: string, isClick: boolean, clientRect: IRect) => void;
@@ -23,6 +30,7 @@ export declare class Selector {
23
30
  private onRequestDeleteConfirm?;
24
31
  private onFreeTextDoubleClick?;
25
32
  private getStylusModeEnabled?;
33
+ private setExternalGesturesEnabled?;
26
34
  private transformerStore;
27
35
  private getAnnotationStore;
28
36
  private konvaCanvasStore;
@@ -32,7 +40,7 @@ export declare class Selector {
32
40
  private marqueeRect;
33
41
  private marqueeStartPos;
34
42
  private marqueeStage;
35
- constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, }: ISelectorOptions);
43
+ constructor({ konvaCanvasStore, getAnnotationStore, onDelete, onSelected, onMultiSelected, onCancel, onChanged, onRequestDeleteConfirm, onFreeTextDoubleClick, getStylusModeEnabled, setExternalGesturesEnabled, }: ISelectorOptions);
36
44
  /** Returns true if this event should be rejected (finger touch in stylus mode). */
37
45
  private isStylusRejected;
38
46
  private handleKeyDown;
@@ -66,6 +74,17 @@ export declare class Selector {
66
74
  activateMarquee(pageNumber: number): void;
67
75
  private bindMarqueeEvents;
68
76
  private rectsIntersect;
77
+ /**
78
+ * Check whether a line segment intersects an axis-aligned rectangle
79
+ * (expanded by `tolerance` on each side to match the visual eraser width).
80
+ * Uses Liang-Barsky line clipping.
81
+ */
82
+ private segmentIntersectsRect;
83
+ /**
84
+ * Check whether any segment of an eraser path intersects a rectangle.
85
+ * Points is a flat array [x0, y0, x1, y1, ...] in page-space coordinates.
86
+ */
87
+ private eraserPathIntersectsRect;
69
88
  activateEraser(pageNumber: number): void;
70
89
  private eraserLine;
71
90
  private eraserStage;
@@ -12,6 +12,7 @@ export class Selector {
12
12
  onRequestDeleteConfirm;
13
13
  onFreeTextDoubleClick;
14
14
  getStylusModeEnabled;
15
+ setExternalGesturesEnabled;
15
16
  transformerStore = /* @__PURE__ */ new Map();
16
17
  getAnnotationStore;
17
18
  konvaCanvasStore;
@@ -31,7 +32,8 @@ export class Selector {
31
32
  onChanged,
32
33
  onRequestDeleteConfirm,
33
34
  onFreeTextDoubleClick,
34
- getStylusModeEnabled
35
+ getStylusModeEnabled,
36
+ setExternalGesturesEnabled
35
37
  }) {
36
38
  this.konvaCanvasStore = konvaCanvasStore;
37
39
  this.getAnnotationStore = getAnnotationStore;
@@ -43,6 +45,7 @@ export class Selector {
43
45
  this.onRequestDeleteConfirm = onRequestDeleteConfirm;
44
46
  this.onFreeTextDoubleClick = onFreeTextDoubleClick;
45
47
  this.getStylusModeEnabled = getStylusModeEnabled;
48
+ this.setExternalGesturesEnabled = setExternalGesturesEnabled;
46
49
  this.handleKeyDown = this.handleKeyDown.bind(this);
47
50
  window.addEventListener("keydown", this.handleKeyDown);
48
51
  }
@@ -188,6 +191,7 @@ export class Selector {
188
191
  const transformer = new Konva.Transformer({
189
192
  resizeEnabled: rawAnnotationStore.resizable,
190
193
  rotateEnabled: false,
194
+ enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
191
195
  borderStrokeWidth: defaultOptions.chooseSetting.STROKEWIDTH,
192
196
  borderStroke: defaultOptions.chooseSetting.COLOR,
193
197
  anchorFill: defaultOptions.chooseSetting.COLOR,
@@ -223,7 +227,34 @@ export class Selector {
223
227
  }
224
228
  transformer.off("transformend");
225
229
  transformer.off("transformstart");
230
+ let dragLayer = null;
231
+ const moveToDragLayer = () => {
232
+ if (dragLayer) return;
233
+ const bg = this.getBackgroundLayer(konvaStage);
234
+ dragLayer = new Konva.Layer();
235
+ konvaStage.add(dragLayer);
236
+ group.moveTo(dragLayer);
237
+ transformer.moveTo(dragLayer);
238
+ bg.batchDraw();
239
+ dragLayer.batchDraw();
240
+ };
241
+ const restoreFromDragLayer = () => {
242
+ if (!dragLayer) return;
243
+ const bg = this.getBackgroundLayer(konvaStage);
244
+ group.moveTo(bg);
245
+ transformer.moveTo(bg);
246
+ dragLayer.destroy();
247
+ dragLayer = null;
248
+ bg.batchDraw();
249
+ };
250
+ transformer.on("transformstart", () => {
251
+ this.setExternalGesturesEnabled?.(false);
252
+ moveToDragLayer();
253
+ this.onCancel();
254
+ });
226
255
  transformer.on("transformend", () => {
256
+ this.setExternalGesturesEnabled?.(true);
257
+ restoreFromDragLayer();
227
258
  this.onChanged(
228
259
  group.id(),
229
260
  group.toJSON(),
@@ -232,13 +263,22 @@ export class Selector {
232
263
  transformer.getClientRect()
233
264
  );
234
265
  });
235
- transformer.on("transformstart", () => {
236
- this.onCancel();
237
- });
266
+ let dragStartRect = null;
267
+ let dragAnchorPos = null;
238
268
  transformer.on("dragstart", () => {
269
+ this.setExternalGesturesEnabled?.(false);
270
+ moveToDragLayer();
239
271
  this.onCancel();
272
+ const boxes = transformer.nodes().map((node) => node.getClientRect());
273
+ dragStartRect = this.getTotalBox(boxes);
274
+ const firstNode = transformer.nodes()[0];
275
+ dragAnchorPos = firstNode ? firstNode.getAbsolutePosition() : null;
240
276
  });
241
277
  transformer.on("dragend", () => {
278
+ this.setExternalGesturesEnabled?.(true);
279
+ restoreFromDragLayer();
280
+ dragStartRect = null;
281
+ dragAnchorPos = null;
242
282
  this.onChanged(
243
283
  group.id(),
244
284
  group.toJSON(),
@@ -248,29 +288,32 @@ export class Selector {
248
288
  );
249
289
  });
250
290
  transformer.on("dragmove", () => {
251
- const boxes = transformer.nodes().map((node) => node.getClientRect());
252
- const box = this.getTotalBox(boxes);
253
- transformer.nodes().forEach((shape) => {
254
- const absPos = shape.getAbsolutePosition();
255
- const offsetX = box.x - absPos.x;
256
- const offsetY = box.y - absPos.y;
257
- const halfWidth = box.width / 2;
258
- const halfHeight = box.height / 2;
259
- const newAbsPos = { ...absPos };
260
- if (box.x + halfWidth < 0) {
261
- newAbsPos.x = -offsetX - halfWidth;
262
- }
263
- if (box.y + halfHeight < 0) {
264
- newAbsPos.y = -offsetY - halfHeight;
265
- }
266
- if (box.x + halfWidth > konvaStage.width()) {
267
- newAbsPos.x = konvaStage.width() - halfWidth - offsetX;
268
- }
269
- if (box.y + halfHeight > konvaStage.height()) {
270
- newAbsPos.y = konvaStage.height() - halfHeight - offsetY;
271
- }
272
- shape.setAbsolutePosition(newAbsPos);
273
- });
291
+ if (!dragStartRect || !dragAnchorPos) return;
292
+ const nodes = transformer.nodes();
293
+ const firstNode = nodes[0];
294
+ if (!firstNode) return;
295
+ const currentPos = firstNode.getAbsolutePosition();
296
+ const dx = currentPos.x - dragAnchorPos.x;
297
+ const dy = currentPos.y - dragAnchorPos.y;
298
+ const boxX = dragStartRect.x + dx;
299
+ const boxY = dragStartRect.y + dy;
300
+ const halfWidth = dragStartRect.width / 2;
301
+ const halfHeight = dragStartRect.height / 2;
302
+ const stageW = konvaStage.width();
303
+ const stageH = konvaStage.height();
304
+ let clampDx = 0;
305
+ let clampDy = 0;
306
+ if (boxX + halfWidth < 0) clampDx = -(boxX + halfWidth);
307
+ else if (boxX + halfWidth > stageW) clampDx = stageW - (boxX + halfWidth);
308
+ if (boxY + halfHeight < 0) clampDy = -(boxY + halfHeight);
309
+ else if (boxY + halfHeight > stageH) clampDy = stageH - (boxY + halfHeight);
310
+ if (clampDx !== 0 || clampDy !== 0) {
311
+ nodes.forEach((shape) => {
312
+ const pos = shape.getAbsolutePosition();
313
+ shape.setAbsolutePosition({ x: pos.x + clampDx, y: pos.y + clampDy });
314
+ });
315
+ dragAnchorPos = firstNode.getAbsolutePosition();
316
+ }
274
317
  });
275
318
  transformer.nodes([group]);
276
319
  this.getBackgroundLayer(konvaStage).add(transformer);
@@ -507,12 +550,11 @@ export class Selector {
507
550
  this.transformerStore.set("__multi__", transformer);
508
551
  nodes.forEach((node) => {
509
552
  node.on("dragstart.marquee", () => {
553
+ this.setExternalGesturesEnabled?.(false);
510
554
  this.onCancel();
511
555
  });
512
- node.on("dragmove.marquee", () => {
513
- this.onMultiSelected(matchedIds, transformer.getClientRect());
514
- });
515
556
  node.on("dragend.marquee", () => {
557
+ this.setExternalGesturesEnabled?.(true);
516
558
  this.onMultiSelected(matchedIds, transformer.getClientRect());
517
559
  });
518
560
  });
@@ -526,6 +568,60 @@ export class Selector {
526
568
  rectsIntersect(a, b) {
527
569
  return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
528
570
  }
571
+ /**
572
+ * Check whether a line segment intersects an axis-aligned rectangle
573
+ * (expanded by `tolerance` on each side to match the visual eraser width).
574
+ * Uses Liang-Barsky line clipping.
575
+ */
576
+ segmentIntersectsRect(x1, y1, x2, y2, rect, tolerance) {
577
+ const left = rect.x - tolerance;
578
+ const top = rect.y - tolerance;
579
+ const right = rect.x + rect.width + tolerance;
580
+ const bottom = rect.y + rect.height + tolerance;
581
+ let tMin = 0;
582
+ let tMax = 1;
583
+ const dx = x2 - x1;
584
+ const dy = y2 - y1;
585
+ const edges = [
586
+ { p: -dx, q: x1 - left },
587
+ { p: dx, q: right - x1 },
588
+ { p: -dy, q: y1 - top },
589
+ { p: dy, q: bottom - y1 }
590
+ ];
591
+ for (const { p, q } of edges) {
592
+ if (p === 0) {
593
+ if (q < 0) return false;
594
+ } else {
595
+ const t = q / p;
596
+ if (p < 0) {
597
+ tMin = Math.max(tMin, t);
598
+ } else {
599
+ tMax = Math.min(tMax, t);
600
+ }
601
+ if (tMin > tMax) return false;
602
+ }
603
+ }
604
+ return true;
605
+ }
606
+ /**
607
+ * Check whether any segment of an eraser path intersects a rectangle.
608
+ * Points is a flat array [x0, y0, x1, y1, ...] in page-space coordinates.
609
+ */
610
+ eraserPathIntersectsRect(points, rect, tolerance) {
611
+ for (let i = 0; i + 3 < points.length; i += 2) {
612
+ if (this.segmentIntersectsRect(
613
+ points[i],
614
+ points[i + 1],
615
+ points[i + 2],
616
+ points[i + 3],
617
+ rect,
618
+ tolerance
619
+ )) {
620
+ return true;
621
+ }
622
+ }
623
+ return false;
624
+ }
529
625
  activateEraser(pageNumber) {
530
626
  const konvaCanvas = this.konvaCanvasStore.get(pageNumber);
531
627
  if (!konvaCanvas) return;
@@ -569,17 +665,20 @@ export class Selector {
569
665
  konvaStage.on("mouseup touchend", (e) => {
570
666
  if (!this.eraserLine || !this.eraserStage) return;
571
667
  if (this.isStylusRejected(e.evt)) return;
572
- const eraserRect = this.eraserLine.getClientRect();
668
+ const points = this.eraserLine.points();
573
669
  this.eraserLine.destroy();
574
670
  this.eraserLine = null;
575
- if (eraserRect.width < 3 && eraserRect.height < 3) {
671
+ if (points.length < 4) {
576
672
  this.eraserStage = null;
577
673
  return;
578
674
  }
675
+ const scale = this.eraserStage.scaleX() ?? 1;
676
+ const screenPoints = points.map((v) => v * scale);
677
+ const tolerance = 6;
579
678
  const groups = this.getPageShapeGroups(this.eraserStage);
580
679
  const toDelete = [];
581
680
  groups.forEach((group) => {
582
- if (this.rectsIntersect(eraserRect, group.getClientRect())) {
681
+ if (this.eraserPathIntersectsRect(screenPoints, group.getClientRect(), tolerance)) {
583
682
  toDelete.push(group.id());
584
683
  }
585
684
  });
@@ -32,11 +32,14 @@ export declare class Painter {
32
32
  private callbacks;
33
33
  private getStylusModeEnabled?;
34
34
  private getReadonly?;
35
- constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, }: {
35
+ private freehandGroupingDelay;
36
+ constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, freehandGroupingDelay, setExternalGesturesEnabled, }: {
36
37
  userName: string;
37
38
  callbacks: PainterCallbacks;
38
39
  getStylusModeEnabled?: () => boolean;
39
40
  getReadonly?: () => boolean;
41
+ freehandGroupingDelay?: number;
42
+ setExternalGesturesEnabled?: (enabled: boolean) => void;
40
43
  });
41
44
  private editFreeText;
42
45
  private bindGlobalEvents;
@@ -10,6 +10,7 @@ import {
10
10
  PAINTER_PAINTING_TYPE
11
11
  } from "./const.js";
12
12
  import { hideCursorPreview } from "./cursor-preview.js";
13
+ import { Editor } from "./editor/editor.js";
13
14
  import { EditorCircle } from "./tools/circle.js";
14
15
  import { EditorFreeHand } from "./tools/freehand.js";
15
16
  import { EditorFreeHighlight } from "./tools/free-highlight.js";
@@ -23,6 +24,10 @@ import { EditorArrow } from "./tools/arrow.js";
23
24
  import { EditorCloud } from "./tools/cloud.js";
24
25
  import { Selector } from "./editor/selector.js";
25
26
  import { Store } from "./store.js";
27
+ import { isIPad } from "./input-device.js";
28
+ if (typeof window !== "undefined" && isIPad()) {
29
+ Konva.pixelRatio = 1;
30
+ }
26
31
  export class Painter {
27
32
  userName;
28
33
  konvaCanvasStore = /* @__PURE__ */ new Map();
@@ -34,16 +39,20 @@ export class Painter {
34
39
  callbacks;
35
40
  getStylusModeEnabled;
36
41
  getReadonly;
42
+ freehandGroupingDelay;
37
43
  constructor({
38
44
  userName,
39
45
  callbacks,
40
46
  getStylusModeEnabled,
41
- getReadonly
47
+ getReadonly,
48
+ freehandGroupingDelay,
49
+ setExternalGesturesEnabled
42
50
  }) {
43
51
  this.userName = userName;
44
52
  this.callbacks = callbacks;
45
53
  this.getStylusModeEnabled = getStylusModeEnabled;
46
54
  this.getReadonly = getReadonly;
55
+ this.freehandGroupingDelay = freehandGroupingDelay ?? 1e3;
47
56
  this.store = new Store();
48
57
  this.selector = new Selector({
49
58
  konvaCanvasStore: this.konvaCanvasStore,
@@ -83,7 +92,8 @@ export class Painter {
83
92
  onFreeTextDoubleClick: (id) => {
84
93
  void this.editFreeText(id);
85
94
  },
86
- getStylusModeEnabled: this.getStylusModeEnabled
95
+ getStylusModeEnabled: this.getStylusModeEnabled,
96
+ setExternalGesturesEnabled
87
97
  });
88
98
  this.bindGlobalEvents();
89
99
  }
@@ -135,7 +145,9 @@ export class Painter {
135
145
  }
136
146
  applyStylusTouchAction(stage) {
137
147
  stage.preventDefault(false);
138
- Konva.pixelRatio = 1;
148
+ if (!isIPad()) {
149
+ Konva.pixelRatio = window.devicePixelRatio || 1;
150
+ }
139
151
  const content = stage.content;
140
152
  if (content) content.style.touchAction = "pan-x pan-y";
141
153
  stage.container().querySelectorAll("canvas").forEach((c) => {
@@ -144,7 +156,9 @@ export class Painter {
144
156
  }
145
157
  removeStylusTouchAction(stage) {
146
158
  stage.preventDefault(true);
147
- Konva.pixelRatio = window.devicePixelRatio || 1;
159
+ if (!isIPad()) {
160
+ Konva.pixelRatio = window.devicePixelRatio || 1;
161
+ }
148
162
  const content = stage.content;
149
163
  if (content) content.style.touchAction = "";
150
164
  stage.container().querySelectorAll("canvas").forEach((c) => {
@@ -244,7 +258,8 @@ export class Painter {
244
258
  onChange: (id, updates) => {
245
259
  this.updateStore(id, updates);
246
260
  },
247
- getStylusModeEnabled: this.getStylusModeEnabled
261
+ getStylusModeEnabled: this.getStylusModeEnabled,
262
+ freehandGroupingDelay: this.freehandGroupingDelay
248
263
  };
249
264
  let editor = null;
250
265
  switch (annotation.type) {
@@ -547,6 +562,7 @@ export class Painter {
547
562
  return this.store.annotations;
548
563
  }
549
564
  getExportContext() {
565
+ Editor.flushAllTimers();
550
566
  const originalIds = this.store.originalIds;
551
567
  const modifiedIds = this.store.modifiedOriginals;
552
568
  const changedAnnotations = this.store.annotations.filter(
@@ -86,7 +86,7 @@ export class EditorFreeHand extends Editor {
86
86
  }
87
87
  });
88
88
  this.currentShapeGroup = null;
89
- });
89
+ }, this.freehandGroupingDelay);
90
90
  return;
91
91
  }
92
92
  Editor.TimerStart(this.pageNumber, () => {
@@ -98,7 +98,7 @@ export class EditorFreeHand extends Editor {
98
98
  }
99
99
  });
100
100
  this.currentShapeGroup = null;
101
- });
101
+ }, this.freehandGroupingDelay);
102
102
  this.line = null;
103
103
  }
104
104
  /**
@@ -3,7 +3,12 @@ type __VLS_Props = {
3
3
  pageNumber: number;
4
4
  pageMeta: PageMeta;
5
5
  scale: number;
6
+ /** Canvas internal resolution multiplier (CSS size unchanged).
7
+ * Used by panzoom to sharpen the PDF when visually zoomed in. */
8
+ renderQuality?: number;
6
9
  };
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>;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ renderQuality: number;
12
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
13
  declare const _default: typeof __VLS_export;
9
14
  export default _default;
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="kviewer-page"
3
+ class="kviewer-page shadow-lg"
4
4
  :style="{
5
5
  position: 'relative',
6
6
  width: cssWidth + 'px',
@@ -63,7 +63,8 @@ const PDFJS_ANNOTATION_MODE_DISABLE = 0;
63
63
  const props = defineProps({
64
64
  pageNumber: { type: Number, required: true },
65
65
  pageMeta: { type: Object, required: true },
66
- scale: { type: Number, required: true }
66
+ scale: { type: Number, required: true },
67
+ renderQuality: { type: Number, required: false, default: 1 }
67
68
  });
68
69
  const canvasRef = ref(null);
69
70
  const textLayerRef = ref(null);
@@ -141,7 +142,14 @@ watch(
141
142
  }
142
143
  }
143
144
  );
144
- async function renderCanvas() {
145
+ watch(
146
+ () => props.renderQuality,
147
+ async () => {
148
+ if (!pageProxy.value) return;
149
+ await renderCanvas({ doubleBuffer: true });
150
+ }
151
+ );
152
+ async function renderCanvas(opts) {
145
153
  const canvas = canvasRef.value;
146
154
  const proxy = pageProxy.value;
147
155
  if (!canvas || !proxy) return;
@@ -150,23 +158,48 @@ async function renderCanvas() {
150
158
  currentRenderTask = null;
151
159
  }
152
160
  const dpr = window.devicePixelRatio || 1;
161
+ const quality = props.renderQuality ?? 1;
153
162
  const viewport = proxy.getViewport({
154
- scale: props.scale * dpr
163
+ scale: props.scale * quality * dpr
155
164
  });
156
- canvas.width = viewport.width;
157
- canvas.height = viewport.height;
158
- try {
159
- currentRenderTask = proxy.render({
160
- canvasContext: canvas.getContext("2d"),
161
- viewport,
162
- annotationMode: PDFJS_ANNOTATION_MODE_DISABLE
163
- });
164
- await currentRenderTask.promise;
165
- } catch (e) {
166
- if (e instanceof Error && e.message?.includes("Rendering cancelled")) return;
167
- throw e;
168
- } finally {
169
- currentRenderTask = null;
165
+ if (opts?.doubleBuffer) {
166
+ const tmp = document.createElement("canvas");
167
+ tmp.width = viewport.width;
168
+ tmp.height = viewport.height;
169
+ try {
170
+ currentRenderTask = proxy.render({
171
+ canvasContext: tmp.getContext("2d"),
172
+ viewport,
173
+ annotationMode: PDFJS_ANNOTATION_MODE_DISABLE
174
+ });
175
+ await currentRenderTask.promise;
176
+ canvas.width = viewport.width;
177
+ canvas.height = viewport.height;
178
+ canvas.getContext("2d").drawImage(tmp, 0, 0);
179
+ } catch (e) {
180
+ if (e instanceof Error && e.message?.includes("Rendering cancelled")) return;
181
+ throw e;
182
+ } finally {
183
+ currentRenderTask = null;
184
+ tmp.width = 0;
185
+ tmp.height = 0;
186
+ }
187
+ } else {
188
+ canvas.width = viewport.width;
189
+ canvas.height = viewport.height;
190
+ try {
191
+ currentRenderTask = proxy.render({
192
+ canvasContext: canvas.getContext("2d"),
193
+ viewport,
194
+ annotationMode: PDFJS_ANNOTATION_MODE_DISABLE
195
+ });
196
+ await currentRenderTask.promise;
197
+ } catch (e) {
198
+ if (e instanceof Error && e.message?.includes("Rendering cancelled")) return;
199
+ throw e;
200
+ } finally {
201
+ currentRenderTask = null;
202
+ }
170
203
  }
171
204
  }
172
205
  function initKonva() {
@@ -3,7 +3,12 @@ type __VLS_Props = {
3
3
  pageNumber: number;
4
4
  pageMeta: PageMeta;
5
5
  scale: number;
6
+ /** Canvas internal resolution multiplier (CSS size unchanged).
7
+ * Used by panzoom to sharpen the PDF when visually zoomed in. */
8
+ renderQuality?: number;
6
9
  };
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>;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ renderQuality: number;
12
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
13
  declare const _default: typeof __VLS_export;
9
14
  export default _default;