kviewer 0.3.0 → 0.3.1

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.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -72,6 +72,15 @@ const module$1 = defineNuxtModule({
72
72
  const require_ = createRequire(import.meta.url);
73
73
  const pdfjsDir = dirname(require_.resolve("pdfjs-dist/package.json"));
74
74
  nuxt.hook("nitro:config", (nitroConfig) => {
75
+ nitroConfig.externals ||= {};
76
+ nitroConfig.externals.inline ||= [];
77
+ if (Array.isArray(nitroConfig.externals.inline)) {
78
+ for (const dependency of ["unhead", "consola"]) {
79
+ if (!nitroConfig.externals.inline.includes(dependency)) {
80
+ nitroConfig.externals.inline.push(dependency);
81
+ }
82
+ }
83
+ }
75
84
  nitroConfig.publicAssets ||= [];
76
85
  nitroConfig.publicAssets.push(
77
86
  {
@@ -44,6 +44,7 @@ export declare const defaultOptions: {
44
44
  STROKE_WIDTH: number;
45
45
  OPACITY: number;
46
46
  MAX_CURSOR_SIZE: number;
47
+ MAX_PLACEMENT_SIZE: number;
47
48
  MAX_UPLOAD_IMAGE_SIZE: number;
48
49
  LOAD_PDF_ANNOTATION: boolean;
49
50
  DB_CLICK_DELETE: boolean;
@@ -74,6 +74,10 @@ export const defaultOptions = {
74
74
  STROKE_WIDTH: 2,
75
75
  OPACITY: 1,
76
76
  MAX_CURSOR_SIZE: 96,
77
+ // Bound for stamps/signatures placed without explicit dimensions. Shared
78
+ // by the placement path and the cursor preview so the ghost cannot drift
79
+ // from what actually lands on the page.
80
+ MAX_PLACEMENT_SIZE: 120,
77
81
  MAX_UPLOAD_IMAGE_SIZE: 800,
78
82
  LOAD_PDF_ANNOTATION: true,
79
83
  DB_CLICK_DELETE: false
@@ -1,2 +1,15 @@
1
- export declare function showCursorPreview(imageUrl: string, size?: number): void;
1
+ export interface ImageSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ /**
6
+ * Bumped whenever the preview is torn down or replaced. Callers that size the
7
+ * preview asynchronously capture it first and bail if it moved, so a slow
8
+ * measurement cannot resurrect a ghost for a tool the user already left.
9
+ */
10
+ export declare function cursorPreviewToken(): number;
11
+ /** Synchronous hit for an already-measured image, so repeat picks don't flash. */
12
+ export declare function peekImageSize(imageUrl: string): ImageSize | null;
13
+ export declare function measureImage(imageUrl: string): Promise<ImageSize | null>;
14
+ export declare function showCursorPreview(imageUrl: string, width?: number, height?: number): void;
2
15
  export declare function hideCursorPreview(): void;
@@ -4,6 +4,32 @@ const PADDING = 6;
4
4
  const CROSSHAIR_SIZE = 12;
5
5
  let previewEl = null;
6
6
  let mouseMoveHandler = null;
7
+ let previewToken = 0;
8
+ const naturalSizeCache = /* @__PURE__ */ new Map();
9
+ export function cursorPreviewToken() {
10
+ return previewToken;
11
+ }
12
+ export function peekImageSize(imageUrl) {
13
+ return naturalSizeCache.get(imageUrl) ?? null;
14
+ }
15
+ export function measureImage(imageUrl) {
16
+ const cached = naturalSizeCache.get(imageUrl);
17
+ if (cached) return Promise.resolve(cached);
18
+ return new Promise((resolve) => {
19
+ const img = new Image();
20
+ img.addEventListener("load", () => {
21
+ const size = { width: img.naturalWidth, height: img.naturalHeight };
22
+ if (!size.width || !size.height) {
23
+ resolve(null);
24
+ return;
25
+ }
26
+ naturalSizeCache.set(imageUrl, size);
27
+ resolve(size);
28
+ });
29
+ img.addEventListener("error", () => resolve(null));
30
+ img.src = imageUrl;
31
+ });
32
+ }
7
33
  function isOverPage(e) {
8
34
  const target = e.target;
9
35
  if (!target) return false;
@@ -21,16 +47,15 @@ function onMouseMove(e) {
21
47
  document.body.style.cursor = "";
22
48
  }
23
49
  }
24
- export function showCursorPreview(imageUrl, size = 80) {
50
+ export function showCursorPreview(imageUrl, width = 80, height = width) {
25
51
  hideCursorPreview();
26
- const outerSize = size + PADDING * 2;
27
52
  previewEl = document.createElement("div");
28
53
  Object.assign(previewEl.style, {
29
54
  position: "fixed",
30
55
  zIndex: "9999",
31
56
  pointerEvents: "none",
32
- width: `${outerSize}px`,
33
- height: `${outerSize}px`,
57
+ width: `${width + PADDING * 2}px`,
58
+ height: `${height + PADDING * 2}px`,
34
59
  transform: "translate(-50%, -50%)",
35
60
  border: `2px solid ${PRIMARY_COLOR}`,
36
61
  borderRadius: "3px",
@@ -41,8 +66,8 @@ export function showCursorPreview(imageUrl, size = 80) {
41
66
  const img = document.createElement("img");
42
67
  img.src = imageUrl;
43
68
  Object.assign(img.style, {
44
- width: `${size}px`,
45
- height: `${size}px`,
69
+ width: `${width}px`,
70
+ height: `${height}px`,
46
71
  objectFit: "contain",
47
72
  display: "block",
48
73
  pointerEvents: "none"
@@ -76,6 +101,7 @@ export function showCursorPreview(imageUrl, size = 80) {
76
101
  document.addEventListener("mousemove", mouseMoveHandler);
77
102
  }
78
103
  export function hideCursorPreview() {
104
+ previewToken++;
79
105
  if (previewEl) {
80
106
  previewEl.remove();
81
107
  previewEl = null;
@@ -1,9 +1,14 @@
1
1
  import Konva from "konva";
2
2
  import { AnnotationType } from "../types.js";
3
- import { resizeImage } from "../utils.js";
4
3
  import { Editor } from "../editor/editor.js";
5
4
  import { defaultOptions } from "../config.js";
6
- import { showCursorPreview } from "../cursor-preview.js";
5
+ import {
6
+ cursorPreviewToken,
7
+ measureImage,
8
+ peekImageSize,
9
+ showCursorPreview
10
+ } from "../cursor-preview.js";
11
+ import { cursorPreviewSize, placementSize } from "../../utils/placement_size.js";
7
12
  export class EditorSignature extends Editor {
8
13
  signatureUrl;
9
14
  signatureImage;
@@ -15,7 +20,23 @@ export class EditorSignature extends Editor {
15
20
  }
16
21
  }
17
22
  createCursorImg() {
18
- showCursorPreview(this.signatureUrl, defaultOptions.setting.MAX_CURSOR_SIZE);
23
+ const scale = this.konvaStage?.scale()?.x ?? 1;
24
+ const max = defaultOptions.setting.MAX_PLACEMENT_SIZE;
25
+ const cached = cursorPreviewSize(null, peekImageSize(this.signatureUrl), max, scale);
26
+ if (cached) {
27
+ showCursorPreview(this.signatureUrl, cached.width, cached.height);
28
+ return;
29
+ }
30
+ const token = cursorPreviewToken();
31
+ void measureImage(this.signatureUrl).then((natural) => {
32
+ if (cursorPreviewToken() !== token) return;
33
+ const size = cursorPreviewSize(null, natural, max, scale);
34
+ if (size) {
35
+ showCursorPreview(this.signatureUrl, size.width, size.height);
36
+ } else {
37
+ showCursorPreview(this.signatureUrl, defaultOptions.setting.MAX_CURSOR_SIZE);
38
+ }
39
+ });
19
40
  }
20
41
  mouseDownHandler(e) {
21
42
  if (e.currentTarget !== this.konvaStage) return;
@@ -25,7 +46,11 @@ export class EditorSignature extends Editor {
25
46
  const pos = this.konvaStage.getRelativePointerPosition();
26
47
  Konva.Image.fromURL(this.signatureUrl, async (image) => {
27
48
  const { width: width_rec, height: height_rec } = image.getClientRect();
28
- const { newWidth, newHeight } = resizeImage(width_rec, height_rec, 120);
49
+ const { width: newWidth, height: newHeight } = placementSize(
50
+ null,
51
+ { width: width_rec, height: height_rec },
52
+ defaultOptions.setting.MAX_PLACEMENT_SIZE
53
+ ) ?? { width: width_rec, height: height_rec };
29
54
  const crosshair = { x: newWidth / 2, y: newHeight / 2 };
30
55
  this.signatureImage = image;
31
56
  this.signatureImage.setAttrs({
@@ -8,6 +8,7 @@ export declare class EditorStamp extends Editor {
8
8
  private stampWidth;
9
9
  private stampHeight;
10
10
  constructor(editorOptions: IEditorOptions, defaultStampUrl: string | null);
11
+ private get explicitSize();
11
12
  private createCursorImg;
12
13
  /**
13
14
  * True when an identical stamp (same source image) already sits under the
@@ -1,9 +1,17 @@
1
1
  import Konva from "konva";
2
2
  import { AnnotationType } from "../types.js";
3
- import { resizeImage } from "../utils.js";
4
3
  import { Editor } from "../editor/editor.js";
5
4
  import { defaultOptions } from "../config.js";
6
- import { showCursorPreview } from "../cursor-preview.js";
5
+ import {
6
+ cursorPreviewToken,
7
+ measureImage,
8
+ peekImageSize,
9
+ showCursorPreview
10
+ } from "../cursor-preview.js";
11
+ import {
12
+ cursorPreviewSize,
13
+ placementSize
14
+ } from "../../utils/placement_size.js";
7
15
  export class EditorStamp extends Editor {
8
16
  stampUrl;
9
17
  stampImage;
@@ -16,10 +24,32 @@ export class EditorStamp extends Editor {
16
24
  this.createCursorImg();
17
25
  }
18
26
  }
27
+ get explicitSize() {
28
+ return { width: this.stampWidth ?? 0, height: this.stampHeight ?? 0 };
29
+ }
19
30
  createCursorImg() {
20
- const baseSize = this.stampWidth ?? this.stampHeight ?? defaultOptions.setting.MAX_CURSOR_SIZE;
21
31
  const scale = this.konvaStage?.scale()?.x ?? 1;
22
- showCursorPreview(this.stampUrl, Math.round(baseSize * scale));
32
+ const max = defaultOptions.setting.MAX_PLACEMENT_SIZE;
33
+ const immediate = cursorPreviewSize(
34
+ this.explicitSize,
35
+ peekImageSize(this.stampUrl),
36
+ max,
37
+ scale
38
+ );
39
+ if (immediate) {
40
+ showCursorPreview(this.stampUrl, immediate.width, immediate.height);
41
+ return;
42
+ }
43
+ const token = cursorPreviewToken();
44
+ void measureImage(this.stampUrl).then((natural) => {
45
+ if (cursorPreviewToken() !== token) return;
46
+ const size = cursorPreviewSize(this.explicitSize, natural, max, scale);
47
+ if (size) {
48
+ showCursorPreview(this.stampUrl, size.width, size.height);
49
+ } else {
50
+ showCursorPreview(this.stampUrl, defaultOptions.setting.MAX_CURSOR_SIZE);
51
+ }
52
+ });
23
53
  }
24
54
  /**
25
55
  * True when an identical stamp (same source image) already sits under the
@@ -47,21 +77,18 @@ export class EditorStamp extends Editor {
47
77
  if (e.currentTarget !== this.konvaStage) return;
48
78
  const pos = this.konvaStage.getRelativePointerPosition();
49
79
  if (!pos) return;
50
- const estimatedSize = this.stampWidth ?? this.stampHeight ?? 120;
80
+ const estimatedSize = this.stampWidth ?? this.stampHeight ?? defaultOptions.setting.MAX_PLACEMENT_SIZE;
51
81
  if (this.hasIdenticalStampNear(pos, estimatedSize)) return;
52
82
  this.stampImage = null;
53
83
  this.currentShapeGroup = this.createShapeGroup();
54
84
  this.getBgLayer().add(this.currentShapeGroup.konvaGroup);
55
85
  Konva.Image.fromURL(this.stampUrl, async (image) => {
56
- let newWidth;
57
- let newHeight;
58
- if (this.stampWidth && this.stampHeight) {
59
- newWidth = this.stampWidth;
60
- newHeight = this.stampHeight;
61
- } else {
62
- const { width: width_rec, height: height_rec } = image.getClientRect();
63
- ({ newWidth, newHeight } = resizeImage(width_rec, height_rec, 120));
64
- }
86
+ const { width: width_rec, height: height_rec } = image.getClientRect();
87
+ const { width: newWidth, height: newHeight } = placementSize(
88
+ this.explicitSize,
89
+ { width: width_rec, height: height_rec },
90
+ defaultOptions.setting.MAX_PLACEMENT_SIZE
91
+ ) ?? { width: width_rec, height: height_rec };
65
92
  const crosshair = { x: newWidth / 2, y: newHeight / 2 };
66
93
  this.stampImage = image;
67
94
  this.stampImage.setAttrs({
@@ -7,10 +7,7 @@ export declare function generateUUID(): string;
7
7
  export declare function setCssCustomProperty(propertyName: string, value: string): void;
8
8
  export declare function removeCssCustomProperty(propertyName: string): void;
9
9
  export declare function base64ToImageBitmap(base64: string): Promise<ImageBitmap>;
10
- export declare function resizeImage(width: number, height: number, max: number): {
11
- newWidth: number;
12
- newHeight: number;
13
- };
10
+ export { resizeImage } from '../utils/placement_size.js';
14
11
  export declare function convertToRGB(array: number[], index?: number): string;
15
12
  export declare function formatTimestamp(timestamp: number): string;
16
13
  export declare function formatPDFDate(dateString: string | null, full?: boolean): string;
@@ -67,16 +67,7 @@ export async function base64ToImageBitmap(base64) {
67
67
  const blob = new Blob([bytes], { type: "image/png" });
68
68
  return await createImageBitmap(blob);
69
69
  }
70
- export function resizeImage(width, height, max) {
71
- if (width <= max && height <= max) {
72
- return { newWidth: width, newHeight: height };
73
- }
74
- const scaleFactor = Math.min(max / width, max / height);
75
- return {
76
- newWidth: width * scaleFactor,
77
- newHeight: height * scaleFactor
78
- };
79
- }
70
+ export { resizeImage } from "../utils/placement_size.js";
80
71
  export function convertToRGB(array, index = 0) {
81
72
  if (index < 0 || index * 3 + 2 >= array.length) {
82
73
  throw new Error("Index out of bounds");
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Sizing rules shared by stamp/signature placement and the drag cursor
3
+ * preview. They live apart from the engine so both paths derive their size
4
+ * from one implementation — the ghost drifting from what actually lands on
5
+ * the page was a real bug, not a hypothetical one.
6
+ */
7
+ export interface ImageSize {
8
+ width: number;
9
+ height: number;
10
+ }
11
+ /** Aspect-preserving downscale. Never upscales. */
12
+ export declare function resizeImage(width: number, height: number, max: number): {
13
+ newWidth: number;
14
+ newHeight: number;
15
+ };
16
+ /**
17
+ * The size an annotation will occupy in stage-content units once placed.
18
+ * Explicit dimensions (a stamp carrying width/height metadata) win outright;
19
+ * otherwise the image's own aspect ratio is bounded by `max`.
20
+ */
21
+ export declare function placementSize(explicit: Partial<ImageSize> | null, natural: ImageSize | null, max: number): ImageSize | null;
22
+ /**
23
+ * The same size expressed in CSS pixels, for the cursor ghost. The Konva stage
24
+ * scale is what converts content units to what the user actually sees, so a
25
+ * ghost that ignores it is wrong at every zoom level but 100%.
26
+ */
27
+ export declare function cursorPreviewSize(explicit: Partial<ImageSize> | null, natural: ImageSize | null, max: number, scale: number): ImageSize | null;
@@ -0,0 +1,26 @@
1
+ export function resizeImage(width, height, max) {
2
+ if (width <= max && height <= max) {
3
+ return { newWidth: width, newHeight: height };
4
+ }
5
+ const scaleFactor = Math.min(max / width, max / height);
6
+ return {
7
+ newWidth: width * scaleFactor,
8
+ newHeight: height * scaleFactor
9
+ };
10
+ }
11
+ export function placementSize(explicit, natural, max) {
12
+ if (explicit?.width && explicit?.height) {
13
+ return { width: explicit.width, height: explicit.height };
14
+ }
15
+ if (!natural?.width || !natural?.height) return null;
16
+ const { newWidth, newHeight } = resizeImage(natural.width, natural.height, max);
17
+ return { width: newWidth, height: newHeight };
18
+ }
19
+ export function cursorPreviewSize(explicit, natural, max, scale) {
20
+ const size = placementSize(explicit, natural, max);
21
+ if (!size) return null;
22
+ return {
23
+ width: Math.round(size.width * scale),
24
+ height: Math.round(size.height * scale)
25
+ };
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",