kviewer 0.0.4 → 0.0.6

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 (63) hide show
  1. package/README.md +3 -0
  2. package/dist/module.json +2 -2
  3. package/dist/runtime/annotation/engine/painter.d.ts +7 -0
  4. package/dist/runtime/annotation/engine/painter.js +14 -0
  5. package/dist/runtime/annotation/engine/store.d.ts +5 -0
  6. package/dist/runtime/annotation/engine/store.js +17 -0
  7. package/dist/runtime/annotation/engine/tools/free-text.js +3 -1
  8. package/dist/runtime/annotation/engine/types.d.ts +20 -0
  9. package/dist/runtime/annotation/pdf-export/export.d.ts +4 -1
  10. package/dist/runtime/annotation/pdf-export/export.js +101 -4
  11. package/dist/runtime/annotation/pdf-export/parse_freetext.js +106 -18
  12. package/dist/runtime/annotation/pdf-export/parse_highlight.js +22 -6
  13. package/dist/runtime/annotation/pdf-export/parse_ink.js +32 -3
  14. package/dist/runtime/annotation/pdf-export/parse_line.js +133 -13
  15. package/dist/runtime/annotation/pdf-import/decode.d.ts +21 -0
  16. package/dist/runtime/annotation/pdf-import/decode.js +134 -0
  17. package/dist/runtime/annotation/pdf-import/decode_circle.d.ts +3 -0
  18. package/dist/runtime/annotation/pdf-import/decode_circle.js +36 -0
  19. package/dist/runtime/annotation/pdf-import/decode_freetext.d.ts +3 -0
  20. package/dist/runtime/annotation/pdf-import/decode_freetext.js +87 -0
  21. package/dist/runtime/annotation/pdf-import/decode_highlight.d.ts +3 -0
  22. package/dist/runtime/annotation/pdf-import/decode_highlight.js +96 -0
  23. package/dist/runtime/annotation/pdf-import/decode_ink.d.ts +3 -0
  24. package/dist/runtime/annotation/pdf-import/decode_ink.js +48 -0
  25. package/dist/runtime/annotation/pdf-import/decode_line.d.ts +3 -0
  26. package/dist/runtime/annotation/pdf-import/decode_line.js +48 -0
  27. package/dist/runtime/annotation/pdf-import/decode_square.d.ts +3 -0
  28. package/dist/runtime/annotation/pdf-import/decode_square.js +39 -0
  29. package/dist/runtime/annotation/pdf-import/decode_stamp.d.ts +3 -0
  30. package/dist/runtime/annotation/pdf-import/decode_stamp.js +38 -0
  31. package/dist/runtime/annotation/pdf-import/decode_text.d.ts +3 -0
  32. package/dist/runtime/annotation/pdf-import/decode_text.js +33 -0
  33. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +23 -0
  34. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +168 -0
  35. package/dist/runtime/annotation/pdf-import/types.d.ts +57 -0
  36. package/dist/runtime/annotation/pdf-import/types.js +25 -0
  37. package/dist/runtime/annotation/pdf-import/utils.d.ts +48 -0
  38. package/dist/runtime/annotation/pdf-import/utils.js +250 -0
  39. package/dist/runtime/assets/kviewer.css +1 -1
  40. package/dist/runtime/components/AnnotationToolbar.vue +1 -0
  41. package/dist/runtime/components/FloatingPageIndicator.vue +4 -1
  42. package/dist/runtime/components/PdfPage.vue +27 -1
  43. package/dist/runtime/components/Viewer.d.vue.ts +12 -1
  44. package/dist/runtime/components/Viewer.vue +114 -35
  45. package/dist/runtime/components/Viewer.vue.d.ts +12 -1
  46. package/dist/runtime/components/ViewerBar.vue +3 -14
  47. package/dist/runtime/components/ViewerTabs.d.vue.ts +16 -1
  48. package/dist/runtime/components/ViewerTabs.vue +42 -12
  49. package/dist/runtime/components/ViewerTabs.vue.d.ts +16 -1
  50. package/dist/runtime/components/form-fields/FormCheckbox.vue +37 -1
  51. package/dist/runtime/components/tools/ActionTools.vue +3 -0
  52. package/dist/runtime/components/tools/PageInfo.vue +1 -1
  53. package/dist/runtime/components/tools/SearchTool.vue +3 -1
  54. package/dist/runtime/components/tools/ZoomControls.vue +3 -0
  55. package/dist/runtime/composables/shape-detection-utils.d.ts +38 -0
  56. package/dist/runtime/composables/shape-detection-utils.js +50 -0
  57. package/dist/runtime/composables/useFormFields.d.ts +3 -1
  58. package/dist/runtime/composables/useFormFields.js +47 -0
  59. package/dist/runtime/composables/useShapeDetection.d.ts +24 -0
  60. package/dist/runtime/composables/useShapeDetection.js +235 -0
  61. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  62. package/dist/runtime/composables/useViewerState.js +13 -1
  63. package/package.json +28 -5
@@ -0,0 +1,235 @@
1
+ import { provide, inject } from "vue";
2
+ import {
3
+ pdfRectToViewport,
4
+ isCheckboxLikePdf,
5
+ isCheckboxLikeWidget,
6
+ makeShape,
7
+ SYMBOL_CHARS
8
+ } from "./shape-detection-utils.js";
9
+ const SHAPE_DETECTION_KEY = Symbol("kviewer-shape-detection");
10
+ let opsPromise = null;
11
+ function getOPS() {
12
+ if (!opsPromise) {
13
+ opsPromise = import("pdfjs-dist/legacy/build/pdf.mjs").then(
14
+ (pdfjs) => pdfjs.OPS
15
+ );
16
+ }
17
+ return opsPromise;
18
+ }
19
+ async function detectAnnotationShapes(pageNumber, pdfPage, viewport) {
20
+ const shapes = [];
21
+ const annotations = await pdfPage.getAnnotations({ intent: "display" });
22
+ for (const annot of annotations) {
23
+ if (!annot.rect) continue;
24
+ const isWidget = annot.fieldType === "Btn" || annot.subtype === "Widget" || annot.subtype === "Square" || annot.subtype === "Circle";
25
+ if (!isWidget) continue;
26
+ const [x1, y1, x2, y2] = annot.rect;
27
+ const { left, top, w, h } = pdfRectToViewport(viewport, x1, y1, x2, y2);
28
+ if (!isCheckboxLikeWidget(w, h)) continue;
29
+ shapes.push(
30
+ makeShape(
31
+ annot.id || `annot_p${pageNumber}_${shapes.length}`,
32
+ left,
33
+ top,
34
+ w,
35
+ h
36
+ )
37
+ );
38
+ }
39
+ return shapes;
40
+ }
41
+ async function detectGraphicalShapes(pageNumber, pdfPage, viewport) {
42
+ const shapes = [];
43
+ try {
44
+ const opList = await pdfPage.getOperatorList();
45
+ const OPS = await getOPS();
46
+ let pathStarted = false;
47
+ let currentPath = [];
48
+ let pathBBox = null;
49
+ const addShape = (x, y, w, h, type) => {
50
+ const aw = Math.abs(w);
51
+ const ah = Math.abs(h);
52
+ if (!isCheckboxLikePdf(aw, ah)) return;
53
+ const { left, top, w: vw, h: vh } = pdfRectToViewport(viewport, x, y, x + w, y + h);
54
+ shapes.push(makeShape(`gfx_p${pageNumber}_${shapes.length}`, left, top, vw, vh, type));
55
+ };
56
+ const checkPathBBox = (type) => {
57
+ if (!pathBBox) return;
58
+ const w = pathBBox.maxX - pathBBox.minX;
59
+ const h = pathBBox.maxY - pathBBox.minY;
60
+ if (!isCheckboxLikePdf(w, h)) return;
61
+ const { left, top, w: vw, h: vh } = pdfRectToViewport(
62
+ viewport,
63
+ pathBBox.minX,
64
+ pathBBox.minY,
65
+ pathBBox.maxX,
66
+ pathBBox.maxY
67
+ );
68
+ shapes.push(makeShape(`gfx_p${pageNumber}_${shapes.length}`, left, top, vw, vh, type));
69
+ };
70
+ const expandBBox = (x, y) => {
71
+ if (!pathBBox) {
72
+ pathBBox = { minX: x, maxX: x, minY: y, maxY: y };
73
+ } else {
74
+ pathBBox.minX = Math.min(pathBBox.minX, x);
75
+ pathBBox.maxX = Math.max(pathBBox.maxX, x);
76
+ pathBBox.minY = Math.min(pathBBox.minY, y);
77
+ pathBBox.maxY = Math.max(pathBBox.maxY, y);
78
+ }
79
+ };
80
+ for (let i = 0; i < opList.fnArray.length; i++) {
81
+ const fn = opList.fnArray[i];
82
+ const args = opList.argsArray[i];
83
+ if (fn === OPS.rectangle) {
84
+ const [x, y, w, h] = args;
85
+ addShape(x, y, w, h, "rectangle");
86
+ } else if (fn === OPS.constructPath) {
87
+ if (args[2] && args[2].length === 4) {
88
+ const bbox = args[2];
89
+ const bw = bbox[2] - bbox[0];
90
+ const bh = bbox[3] - bbox[1];
91
+ addShape(bbox[0], bbox[1], bw, bh, "rectangle");
92
+ } else if (Array.isArray(args[0])) {
93
+ const ops = args[0];
94
+ const coords = args[1];
95
+ if (!ops || !coords) continue;
96
+ currentPath = [];
97
+ pathBBox = null;
98
+ let ci = 0;
99
+ for (let oi = 0; oi < ops.length; oi++) {
100
+ const op = ops[oi];
101
+ if (op === OPS.moveTo) {
102
+ const x = coords[ci++];
103
+ const y = coords[ci++];
104
+ currentPath.push([x, y]);
105
+ expandBBox(x, y);
106
+ } else if (op === OPS.lineTo) {
107
+ const x = coords[ci++];
108
+ const y = coords[ci++];
109
+ currentPath.push([x, y]);
110
+ expandBBox(x, y);
111
+ } else if (op === OPS.rectangle) {
112
+ const x = coords[ci++];
113
+ const y = coords[ci++];
114
+ const w = coords[ci++];
115
+ const h = coords[ci++];
116
+ addShape(x, y, w, h, "rectangle");
117
+ } else if (op === OPS.curveTo) {
118
+ ci += 6;
119
+ } else if (op === OPS.curveTo2) {
120
+ ci += 4;
121
+ } else if (op === OPS.curveTo3) {
122
+ ci += 4;
123
+ }
124
+ }
125
+ if (pathBBox && currentPath.length >= 4) {
126
+ checkPathBBox(currentPath.length > 8 ? "circle" : "rectangle");
127
+ }
128
+ pathStarted = true;
129
+ }
130
+ } else if (fn === OPS.moveTo) {
131
+ pathStarted = true;
132
+ currentPath = [[args[0], args[1]]];
133
+ pathBBox = null;
134
+ expandBBox(args[0], args[1]);
135
+ } else if (fn === OPS.lineTo && pathStarted) {
136
+ currentPath.push([args[0], args[1]]);
137
+ expandBBox(args[0], args[1]);
138
+ } else if (pathStarted && (fn === OPS.stroke || fn === OPS.fill || fn === OPS.eoFill || fn === OPS.fillStroke)) {
139
+ if ((currentPath.length === 5 || currentPath.length > 8) && pathBBox) {
140
+ checkPathBBox(currentPath.length > 8 ? "circle" : "rectangle");
141
+ }
142
+ pathStarted = false;
143
+ currentPath = [];
144
+ pathBBox = null;
145
+ }
146
+ }
147
+ } catch (err) {
148
+ console.error("[ShapeDetection] Graphical detection error:", err);
149
+ }
150
+ return shapes;
151
+ }
152
+ async function detectIconFontShapes(pageNumber, pdfPage, viewport) {
153
+ const shapes = [];
154
+ try {
155
+ const textContent = await pdfPage.getTextContent();
156
+ for (const item of textContent.items) {
157
+ if (!("str" in item) || !item.str || !("transform" in item) || !item.transform) continue;
158
+ const char = item.str;
159
+ const code = char.charCodeAt(0);
160
+ const isSymbol = SYMBOL_CHARS.has(char);
161
+ const isBox = code >= 9472 && code <= 9599;
162
+ const isGeo = code >= 9632 && code <= 9727;
163
+ const isPUA = code >= 57344 && code <= 63743;
164
+ if (!isSymbol && !isBox && !isGeo && !isPUA) continue;
165
+ const t = item.transform;
166
+ const w = item.width || Math.abs(t[0]);
167
+ const h = item.height || Math.abs(t[3]);
168
+ const x = t[4];
169
+ const y = t[5];
170
+ const minSize = isPUA ? 12 : 5;
171
+ if (w < minSize || w > 40 || h < minSize || h > 40) continue;
172
+ if (isPUA) {
173
+ const ratio = w / h;
174
+ if (ratio < 0.8 || ratio > 1.25) continue;
175
+ }
176
+ const { left, top, w: vw, h: vh } = pdfRectToViewport(viewport, x, y, x + w, y + h);
177
+ const type = isPUA ? "circle" : void 0;
178
+ shapes.push(makeShape(`icon_p${pageNumber}_${shapes.length}`, left, top, vw, vh, type));
179
+ }
180
+ } catch (err) {
181
+ console.error("[ShapeDetection] Icon-font detection error:", err);
182
+ }
183
+ return shapes;
184
+ }
185
+ export function createShapeDetection() {
186
+ const shapeCache = /* @__PURE__ */ new Map();
187
+ async function preprocessShapesForPage(pageNumber, pdfPage, viewport) {
188
+ try {
189
+ const [annotShapes, gfxShapes, iconShapes] = await Promise.all([
190
+ detectAnnotationShapes(pageNumber, pdfPage, viewport),
191
+ detectGraphicalShapes(pageNumber, pdfPage, viewport),
192
+ detectIconFontShapes(pageNumber, pdfPage, viewport)
193
+ ]);
194
+ const shapes = [...annotShapes, ...gfxShapes, ...iconShapes];
195
+ shapeCache.set(pageNumber, shapes);
196
+ if (import.meta.dev) {
197
+ console.debug(
198
+ `[ShapeDetection] Page ${pageNumber}: ${shapes.length} shapes (${annotShapes.length} annot, ${gfxShapes.length} gfx, ${iconShapes.length} icon)`
199
+ );
200
+ }
201
+ return shapes;
202
+ } catch (err) {
203
+ console.error("[ShapeDetection] preprocessShapesForPage error:", err);
204
+ shapeCache.set(pageNumber, []);
205
+ return [];
206
+ }
207
+ }
208
+ function findShapeAtPoint(pageNumber, x, y) {
209
+ const shapes = shapeCache.get(pageNumber) || [];
210
+ return shapes.find(
211
+ (s) => x >= s.rect.x && x <= s.rect.x + s.rect.w && y >= s.rect.y && y <= s.rect.y + s.rect.h
212
+ ) || null;
213
+ }
214
+ function clearShapeCache(pageNumber) {
215
+ shapeCache.delete(pageNumber);
216
+ }
217
+ function clearAllShapeCache() {
218
+ shapeCache.clear();
219
+ }
220
+ const detection = {
221
+ preprocessShapesForPage,
222
+ findShapeAtPoint,
223
+ clearShapeCache,
224
+ clearAllShapeCache
225
+ };
226
+ provide(SHAPE_DETECTION_KEY, detection);
227
+ return detection;
228
+ }
229
+ export function useShapeDetection() {
230
+ const detection = inject(SHAPE_DETECTION_KEY);
231
+ if (!detection) {
232
+ throw new Error("useShapeDetection() must be used inside a <KViewer> component");
233
+ }
234
+ return detection;
235
+ }
@@ -26,6 +26,7 @@ export interface ViewerState {
26
26
  currentPage: Ref<number>;
27
27
  totalPages: Ref<number>;
28
28
  isLoading: Ref<boolean>;
29
+ error: Ref<string | null>;
29
30
  activeTool: Ref<ActiveTool>;
30
31
  doc: ShallowRef<PDFDocumentProxy | null>;
31
32
  annotations: ShallowRef<Map<string, IAnnotationStore>>;
@@ -52,12 +53,15 @@ export interface ViewerState {
52
53
  stylusMode: Ref<boolean>;
53
54
  setStylusMode: (enabled: boolean) => void;
54
55
  scrollToPage: (pageNumber: number) => void;
56
+ downloadPdf: () => Promise<void>;
55
57
  history: AnnotationHistory;
56
58
  readonly: Ref<boolean>;
59
+ shapeDetection: Ref<boolean>;
57
60
  }
58
61
  export interface ViewerStateProvider {
59
62
  state: ViewerState;
60
63
  setScrollToPageFn: (fn: (pageNumber: number) => void) => void;
64
+ setDownloadPdfFn: (fn: () => Promise<void>) => void;
61
65
  }
62
66
  export declare function provideViewerState(): ViewerStateProvider;
63
67
  export declare function useViewerState(): ViewerState;
@@ -20,6 +20,7 @@ export function provideViewerState() {
20
20
  const currentPage = ref(1);
21
21
  const totalPages = ref(0);
22
22
  const isLoading = ref(false);
23
+ const error = ref(null);
23
24
  const activeTool = ref("hand");
24
25
  const doc = shallowRef(null);
25
26
  const annotations = shallowRef(/* @__PURE__ */ new Map());
@@ -63,6 +64,7 @@ export function provideViewerState() {
63
64
  }
64
65
  }
65
66
  const readonly = ref(false);
67
+ const shapeDetectionEnabled = ref(false);
66
68
  const stylusMode = ref(false);
67
69
  function setStylusMode(enabled) {
68
70
  stylusMode.value = enabled;
@@ -128,9 +130,13 @@ export function provideViewerState() {
128
130
  return painter.value?.getData() ?? [];
129
131
  }
130
132
  let _scrollToPageFn = null;
133
+ let _downloadPdfFn = null;
131
134
  function scrollToPage(pageNumber) {
132
135
  _scrollToPageFn?.(pageNumber);
133
136
  }
137
+ async function downloadPdf() {
138
+ await _downloadPdfFn?.();
139
+ }
134
140
  const history = createAnnotationHistory(annotations, painter);
135
141
  const state = {
136
142
  scale,
@@ -145,6 +151,7 @@ export function provideViewerState() {
145
151
  currentPage,
146
152
  totalPages,
147
153
  isLoading,
154
+ error,
148
155
  activeTool,
149
156
  doc,
150
157
  annotations,
@@ -171,14 +178,19 @@ export function provideViewerState() {
171
178
  stylusMode,
172
179
  setStylusMode,
173
180
  scrollToPage,
181
+ downloadPdf,
174
182
  history,
175
- readonly
183
+ readonly,
184
+ shapeDetection: shapeDetectionEnabled
176
185
  };
177
186
  provide(VIEWER_STATE_KEY, state);
178
187
  return {
179
188
  state,
180
189
  setScrollToPageFn(fn) {
181
190
  _scrollToPageFn = fn;
191
+ },
192
+ setDownloadPdfFn(fn) {
193
+ _downloadPdfFn = fn;
182
194
  }
183
195
  };
184
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",
@@ -23,7 +23,8 @@
23
23
  "dist"
24
24
  ],
25
25
  "workspaces": [
26
- "playground"
26
+ "playground",
27
+ "docs"
27
28
  ],
28
29
  "scripts": {
29
30
  "prepack": "nuxt-module-build build",
@@ -32,9 +33,11 @@
32
33
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
33
34
  "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
34
35
  "lint": "eslint .",
35
- "test": "vitest run",
36
+ "test": "vitest run --exclude '**/e2e/**'",
36
37
  "test:watch": "vitest watch",
37
- "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
38
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
39
+ "test:e2e": "playwright test",
40
+ "test:e2e:ui": "playwright test --ui"
38
41
  },
39
42
  "dependencies": {
40
43
  "@nuxt/kit": "^4.4.2",
@@ -52,13 +55,33 @@
52
55
  "@nuxt/schema": "^4.4.2",
53
56
  "@nuxt/test-utils": "^4.0.0",
54
57
  "@nuxt/ui": "4.6.0",
58
+ "@playwright/test": "^1.59.1",
55
59
  "@types/node": "latest",
60
+ "canvas": "^3.2.3",
56
61
  "changelogen": "^0.6.2",
57
62
  "eslint": "^10.1.0",
58
63
  "nuxt": "^4.4.2",
64
+ "path2d": "^0.3.1",
65
+ "pixelmatch": "^7.1.0",
66
+ "pngjs": "^7.0.0",
59
67
  "typescript": "~6.0.2",
60
68
  "vitest": "^4.1.1",
61
69
  "vue-tsc": "^3.2.6"
62
70
  },
63
- "packageManager": "pnpm@10.33.0"
71
+ "packageManager": "pnpm@10.33.0",
72
+ "pnpm": {
73
+ "onlyBuiltDependencies": [
74
+ "canvas",
75
+ "@parcel/watcher",
76
+ "better-sqlite3",
77
+ "esbuild",
78
+ "sharp",
79
+ "vue-demi",
80
+ "unrs-resolver"
81
+ ],
82
+ "overrides": {
83
+ "vue": "^3.5.32",
84
+ "vue-router": "^5.0.4"
85
+ }
86
+ }
64
87
  }