kviewer 0.0.4 → 0.0.5
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/README.md +3 -0
- package/dist/module.json +2 -2
- package/dist/runtime/annotation/engine/painter.d.ts +7 -0
- package/dist/runtime/annotation/engine/painter.js +14 -0
- package/dist/runtime/annotation/engine/store.d.ts +5 -0
- package/dist/runtime/annotation/engine/store.js +17 -0
- package/dist/runtime/annotation/engine/tools/free-text.js +3 -1
- package/dist/runtime/annotation/engine/types.d.ts +20 -0
- package/dist/runtime/annotation/pdf-export/export.d.ts +4 -1
- package/dist/runtime/annotation/pdf-export/export.js +101 -4
- package/dist/runtime/annotation/pdf-export/parse_freetext.js +106 -18
- package/dist/runtime/annotation/pdf-export/parse_highlight.js +22 -6
- package/dist/runtime/annotation/pdf-export/parse_ink.js +32 -3
- package/dist/runtime/annotation/pdf-export/parse_line.js +133 -13
- package/dist/runtime/annotation/pdf-import/decode.d.ts +21 -0
- package/dist/runtime/annotation/pdf-import/decode.js +134 -0
- package/dist/runtime/annotation/pdf-import/decode_circle.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_circle.js +36 -0
- package/dist/runtime/annotation/pdf-import/decode_freetext.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_freetext.js +87 -0
- package/dist/runtime/annotation/pdf-import/decode_highlight.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_highlight.js +96 -0
- package/dist/runtime/annotation/pdf-import/decode_ink.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_ink.js +48 -0
- package/dist/runtime/annotation/pdf-import/decode_line.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_line.js +48 -0
- package/dist/runtime/annotation/pdf-import/decode_square.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_square.js +39 -0
- package/dist/runtime/annotation/pdf-import/decode_stamp.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_stamp.js +38 -0
- package/dist/runtime/annotation/pdf-import/decode_text.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_text.js +33 -0
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +23 -0
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +168 -0
- package/dist/runtime/annotation/pdf-import/types.d.ts +57 -0
- package/dist/runtime/annotation/pdf-import/types.js +25 -0
- package/dist/runtime/annotation/pdf-import/utils.d.ts +48 -0
- package/dist/runtime/annotation/pdf-import/utils.js +250 -0
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/AnnotationToolbar.vue +1 -0
- package/dist/runtime/components/FloatingPageIndicator.vue +4 -1
- package/dist/runtime/components/PdfPage.vue +27 -1
- package/dist/runtime/components/Viewer.d.vue.ts +12 -1
- package/dist/runtime/components/Viewer.vue +112 -34
- package/dist/runtime/components/Viewer.vue.d.ts +12 -1
- package/dist/runtime/components/ViewerBar.vue +8 -3
- package/dist/runtime/components/ViewerTabs.d.vue.ts +16 -1
- package/dist/runtime/components/ViewerTabs.vue +42 -12
- package/dist/runtime/components/ViewerTabs.vue.d.ts +16 -1
- package/dist/runtime/components/form-fields/FormCheckbox.vue +37 -1
- package/dist/runtime/components/tools/ActionTools.vue +3 -0
- package/dist/runtime/components/tools/PageInfo.vue +1 -1
- package/dist/runtime/components/tools/SearchTool.vue +3 -1
- package/dist/runtime/components/tools/ZoomControls.vue +3 -0
- package/dist/runtime/composables/shape-detection-utils.d.ts +38 -0
- package/dist/runtime/composables/shape-detection-utils.js +50 -0
- package/dist/runtime/composables/useFormFields.d.ts +3 -1
- package/dist/runtime/composables/useFormFields.js +47 -0
- package/dist/runtime/composables/useShapeDetection.d.ts +24 -0
- package/dist/runtime/composables/useShapeDetection.js +235 -0
- package/dist/runtime/composables/useViewerState.d.ts +2 -0
- package/dist/runtime/composables/useViewerState.js +5 -1
- package/dist/runtime/plugin.d.ts +1 -1
- package/package.json +28 -5
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<!-- Top navigation bar -->
|
|
4
4
|
<div class="flex items-center h-[40px] px-3 gap-2 bg-elevated/50">
|
|
5
5
|
<UPopover>
|
|
6
|
-
<UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" />
|
|
6
|
+
<UButton icon="i-lucide-menu" variant="ghost" color="neutral" size="xs" data-testid="viewer-menu" />
|
|
7
7
|
<template #content>
|
|
8
8
|
<div class="p-1">
|
|
9
9
|
<UButton
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
color="neutral"
|
|
14
14
|
size="xs"
|
|
15
15
|
class="w-full justify-start"
|
|
16
|
+
data-testid="viewer-download"
|
|
16
17
|
@click="onDownload"
|
|
17
18
|
/>
|
|
18
19
|
</div>
|
|
@@ -77,13 +78,17 @@ function bumpStyleVersion() {
|
|
|
77
78
|
async function onDownload() {
|
|
78
79
|
try {
|
|
79
80
|
if (!state.doc.value || !state.painter.value) return;
|
|
81
|
+
const exportContext = state.painter.value.getExportContext();
|
|
80
82
|
const bytes = await exportAnnotationsToPdf({
|
|
81
83
|
pdfData: await state.doc.value.getData(),
|
|
82
|
-
annotations:
|
|
84
|
+
annotations: exportContext.annotations,
|
|
83
85
|
options: {
|
|
84
86
|
flatten: false,
|
|
85
87
|
preserveOriginalAnnotations: false
|
|
86
|
-
}
|
|
88
|
+
},
|
|
89
|
+
originalAnnotationIds: exportContext.originalIds,
|
|
90
|
+
modifiedAnnotationIds: exportContext.modifiedIds,
|
|
91
|
+
deletedAnnotationIds: exportContext.deletedIds
|
|
87
92
|
});
|
|
88
93
|
downloadPdfBytes(bytes, `annotated_${getTimestampString()}.pdf`);
|
|
89
94
|
} catch (error) {
|
|
@@ -16,6 +16,8 @@ export interface ViewerTabItem {
|
|
|
16
16
|
viewMode?: ViewMode;
|
|
17
17
|
/** Zoom override for this document. */
|
|
18
18
|
zoom?: number;
|
|
19
|
+
/** Enable shape detection for this document. */
|
|
20
|
+
shapeDetection?: boolean;
|
|
19
21
|
}
|
|
20
22
|
export interface AddTabOptions {
|
|
21
23
|
/** Insert at specific index. Defaults to end. */
|
|
@@ -38,6 +40,8 @@ type __VLS_Props = {
|
|
|
38
40
|
textLayer?: boolean;
|
|
39
41
|
/** When true, all viewers are in view-only mode. */
|
|
40
42
|
readonly?: boolean;
|
|
43
|
+
/** Enable shape detection on all Viewer instances. Can be overridden per tab. */
|
|
44
|
+
shapeDetection?: boolean;
|
|
41
45
|
/** Default view mode for tabs without a per-tab override. */
|
|
42
46
|
viewMode?: ViewMode;
|
|
43
47
|
/** Default zoom for tabs without a per-tab override. */
|
|
@@ -148,6 +152,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
|
|
|
148
152
|
closable?: boolean | undefined;
|
|
149
153
|
viewMode?: ViewMode | undefined;
|
|
150
154
|
zoom?: number | undefined;
|
|
155
|
+
shapeDetection?: boolean | undefined;
|
|
151
156
|
};
|
|
152
157
|
}, __VLS_33: {
|
|
153
158
|
tab: {
|
|
@@ -246,6 +251,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
|
|
|
246
251
|
closable?: boolean | undefined;
|
|
247
252
|
viewMode?: ViewMode | undefined;
|
|
248
253
|
zoom?: number | undefined;
|
|
254
|
+
shapeDetection?: boolean | undefined;
|
|
249
255
|
};
|
|
250
256
|
}, __VLS_35: {};
|
|
251
257
|
type __VLS_Slots = {} & {
|
|
@@ -361,6 +367,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
361
367
|
closable?: boolean | undefined;
|
|
362
368
|
viewMode?: ViewMode | undefined;
|
|
363
369
|
zoom?: number | undefined;
|
|
370
|
+
shapeDetection?: boolean | undefined;
|
|
364
371
|
}[];
|
|
365
372
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
366
373
|
"update:activeTab": (id: string) => any;
|
|
@@ -372,7 +379,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
372
379
|
"onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
|
|
373
380
|
"onTab-close"?: ((id: string) => any) | undefined;
|
|
374
381
|
"onTab-removed"?: ((id: string) => any) | undefined;
|
|
375
|
-
}>, {
|
|
382
|
+
}>, {
|
|
383
|
+
userName: string;
|
|
384
|
+
zoom: number;
|
|
385
|
+
stamps: StampDefinition[];
|
|
386
|
+
signatureHandlers: SignatureHandlers;
|
|
387
|
+
viewMode: ViewMode;
|
|
388
|
+
defaultActiveTab: string;
|
|
389
|
+
minTabs: number;
|
|
390
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
376
391
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
377
392
|
declare const _default: typeof __VLS_export;
|
|
378
393
|
export default _default;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
:class="[
|
|
16
16
|
tab.id === activeTabId ? 'bg-elevated text-highlighted border-b-2 border-b-primary' : 'text-muted hover:text-highlighted hover:bg-elevated/80'
|
|
17
17
|
]"
|
|
18
|
+
:data-testid="'tab-' + tab.id"
|
|
18
19
|
@click="activateTab(tab.id)"
|
|
19
20
|
>
|
|
20
21
|
<UIcon
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
color="neutral"
|
|
31
32
|
class="opacity-0 group-hover:opacity-100 transition-opacity -mr-1"
|
|
32
33
|
:class="{ 'opacity-100': tab.id === activeTabId }"
|
|
34
|
+
:data-testid="'tab-close-' + tab.id"
|
|
33
35
|
@click.stop="removeTab(tab.id)"
|
|
34
36
|
/>
|
|
35
37
|
</button>
|
|
@@ -38,31 +40,31 @@
|
|
|
38
40
|
</div>
|
|
39
41
|
</ClientOnly>
|
|
40
42
|
|
|
41
|
-
<!-- Viewer container -->
|
|
43
|
+
<!-- Viewer container — only the active tab is rendered -->
|
|
42
44
|
<div class="flex-1 relative overflow-hidden">
|
|
43
45
|
<div
|
|
44
|
-
v-
|
|
45
|
-
|
|
46
|
-
:key="tab.id"
|
|
46
|
+
v-if="activeTab"
|
|
47
|
+
:key="activeTab.id"
|
|
47
48
|
class="absolute inset-0"
|
|
48
49
|
>
|
|
49
50
|
<Viewer
|
|
50
|
-
:ref="(el) => setViewerRef(
|
|
51
|
-
:source="
|
|
52
|
-
:active="
|
|
51
|
+
:ref="(el) => setViewerRef(activeTab.id, el)"
|
|
52
|
+
:source="activeTab.source"
|
|
53
|
+
:active="true"
|
|
53
54
|
:stamps="stamps"
|
|
54
55
|
:text-layer="textLayer"
|
|
55
56
|
:user-name="userName"
|
|
56
57
|
:signature-handlers="signatureHandlers"
|
|
57
|
-
:view-mode="
|
|
58
|
-
:zoom="
|
|
58
|
+
:view-mode="activeTab.viewMode ?? viewMode"
|
|
59
|
+
:zoom="activeTab.zoom ?? zoom"
|
|
59
60
|
:readonly="readonly"
|
|
61
|
+
:shape-detection="activeTab.shapeDetection ?? shapeDetection"
|
|
60
62
|
>
|
|
61
63
|
<template v-if="$slots.header" #header>
|
|
62
|
-
<slot name="header" :tab="
|
|
64
|
+
<slot name="header" :tab="activeTab" />
|
|
63
65
|
</template>
|
|
64
66
|
<template v-if="$slots.footer" #footer>
|
|
65
|
-
<slot name="footer" :tab="
|
|
67
|
+
<slot name="footer" :tab="activeTab" />
|
|
66
68
|
</template>
|
|
67
69
|
</Viewer>
|
|
68
70
|
</div>
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
</template>
|
|
77
79
|
|
|
78
80
|
<script setup>
|
|
79
|
-
import { ref, watch } from "vue";
|
|
81
|
+
import { ref, computed, watch, nextTick } from "vue";
|
|
80
82
|
import Viewer from "./Viewer.vue";
|
|
81
83
|
const props = defineProps({
|
|
82
84
|
items: { type: Array, required: true },
|
|
@@ -86,6 +88,7 @@ const props = defineProps({
|
|
|
86
88
|
signatureHandlers: { type: Object, required: false, default: void 0 },
|
|
87
89
|
textLayer: { type: Boolean, required: false },
|
|
88
90
|
readonly: { type: Boolean, required: false },
|
|
91
|
+
shapeDetection: { type: Boolean, required: false },
|
|
89
92
|
viewMode: { type: String, required: false, default: "fit-width" },
|
|
90
93
|
zoom: { type: Number, required: false, default: 1 },
|
|
91
94
|
minTabs: { type: Number, required: false, default: 0 }
|
|
@@ -99,14 +102,37 @@ const tabs = ref([...props.items]);
|
|
|
99
102
|
const activeTabId = ref(
|
|
100
103
|
props.defaultActiveTab ?? props.items[0]?.id ?? ""
|
|
101
104
|
);
|
|
105
|
+
const activeTab = computed(
|
|
106
|
+
() => tabs.value.find((t) => t.id === activeTabId.value) ?? null
|
|
107
|
+
);
|
|
102
108
|
const viewerRefs = /* @__PURE__ */ new Map();
|
|
109
|
+
const savedAnnotations = /* @__PURE__ */ new Map();
|
|
103
110
|
function setViewerRef(id, el) {
|
|
104
111
|
if (el) {
|
|
105
112
|
viewerRefs.set(id, el);
|
|
113
|
+
const saved = savedAnnotations.get(id);
|
|
114
|
+
if (saved && saved.length > 0) {
|
|
115
|
+
nextTick(() => {
|
|
116
|
+
const viewer = el;
|
|
117
|
+
if (typeof viewer.importAnnotations === "function") {
|
|
118
|
+
viewer.importAnnotations(saved, { mode: "merge" }).catch(() => {
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
106
123
|
} else {
|
|
107
124
|
viewerRefs.delete(id);
|
|
108
125
|
}
|
|
109
126
|
}
|
|
127
|
+
function saveViewerState(id) {
|
|
128
|
+
const viewer = viewerRefs.get(id);
|
|
129
|
+
if (viewer && typeof viewer.getAnnotations === "function") {
|
|
130
|
+
const annotations = viewer.getAnnotations();
|
|
131
|
+
if (annotations.length > 0) {
|
|
132
|
+
savedAnnotations.set(id, annotations);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
110
136
|
watch(
|
|
111
137
|
() => props.items,
|
|
112
138
|
(newItems) => {
|
|
@@ -139,6 +165,7 @@ function removeTab(id) {
|
|
|
139
165
|
emit("tab-close", id);
|
|
140
166
|
tabs.value.splice(index, 1);
|
|
141
167
|
viewerRefs.delete(id);
|
|
168
|
+
savedAnnotations.delete(id);
|
|
142
169
|
if (activeTabId.value === id) {
|
|
143
170
|
const newIndex = Math.min(index, tabs.value.length - 1);
|
|
144
171
|
activeTabId.value = tabs.value[newIndex]?.id ?? "";
|
|
@@ -151,6 +178,9 @@ function removeTab(id) {
|
|
|
151
178
|
}
|
|
152
179
|
function activateTab(id) {
|
|
153
180
|
if (tabs.value.some((t) => t.id === id)) {
|
|
181
|
+
if (activeTabId.value && activeTabId.value !== id) {
|
|
182
|
+
saveViewerState(activeTabId.value);
|
|
183
|
+
}
|
|
154
184
|
activeTabId.value = id;
|
|
155
185
|
emit("update:activeTab", id);
|
|
156
186
|
}
|
|
@@ -16,6 +16,8 @@ export interface ViewerTabItem {
|
|
|
16
16
|
viewMode?: ViewMode;
|
|
17
17
|
/** Zoom override for this document. */
|
|
18
18
|
zoom?: number;
|
|
19
|
+
/** Enable shape detection for this document. */
|
|
20
|
+
shapeDetection?: boolean;
|
|
19
21
|
}
|
|
20
22
|
export interface AddTabOptions {
|
|
21
23
|
/** Insert at specific index. Defaults to end. */
|
|
@@ -38,6 +40,8 @@ type __VLS_Props = {
|
|
|
38
40
|
textLayer?: boolean;
|
|
39
41
|
/** When true, all viewers are in view-only mode. */
|
|
40
42
|
readonly?: boolean;
|
|
43
|
+
/** Enable shape detection on all Viewer instances. Can be overridden per tab. */
|
|
44
|
+
shapeDetection?: boolean;
|
|
41
45
|
/** Default view mode for tabs without a per-tab override. */
|
|
42
46
|
viewMode?: ViewMode;
|
|
43
47
|
/** Default zoom for tabs without a per-tab override. */
|
|
@@ -148,6 +152,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
|
|
|
148
152
|
closable?: boolean | undefined;
|
|
149
153
|
viewMode?: ViewMode | undefined;
|
|
150
154
|
zoom?: number | undefined;
|
|
155
|
+
shapeDetection?: boolean | undefined;
|
|
151
156
|
};
|
|
152
157
|
}, __VLS_33: {
|
|
153
158
|
tab: {
|
|
@@ -246,6 +251,7 @@ declare var __VLS_7: {}, __VLS_21: {}, __VLS_30: {
|
|
|
246
251
|
closable?: boolean | undefined;
|
|
247
252
|
viewMode?: ViewMode | undefined;
|
|
248
253
|
zoom?: number | undefined;
|
|
254
|
+
shapeDetection?: boolean | undefined;
|
|
249
255
|
};
|
|
250
256
|
}, __VLS_35: {};
|
|
251
257
|
type __VLS_Slots = {} & {
|
|
@@ -361,6 +367,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
361
367
|
closable?: boolean | undefined;
|
|
362
368
|
viewMode?: ViewMode | undefined;
|
|
363
369
|
zoom?: number | undefined;
|
|
370
|
+
shapeDetection?: boolean | undefined;
|
|
364
371
|
}[];
|
|
365
372
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
366
373
|
"update:activeTab": (id: string) => any;
|
|
@@ -372,7 +379,15 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
372
379
|
"onTab-added"?: ((tab: ViewerTabItem) => any) | undefined;
|
|
373
380
|
"onTab-close"?: ((id: string) => any) | undefined;
|
|
374
381
|
"onTab-removed"?: ((id: string) => any) | undefined;
|
|
375
|
-
}>, {
|
|
382
|
+
}>, {
|
|
383
|
+
userName: string;
|
|
384
|
+
zoom: number;
|
|
385
|
+
stamps: StampDefinition[];
|
|
386
|
+
signatureHandlers: SignatureHandlers;
|
|
387
|
+
viewMode: ViewMode;
|
|
388
|
+
defaultActiveTab: string;
|
|
389
|
+
minTabs: number;
|
|
390
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
376
391
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
377
392
|
declare const _default: typeof __VLS_export;
|
|
378
393
|
export default _default;
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
2
|
+
<!-- Detected shape: custom × icon on transparent background -->
|
|
3
|
+
<div
|
|
4
|
+
v-if="isDetected"
|
|
5
|
+
class="kviewer-form-checkbox kviewer-form-checkbox--detected"
|
|
6
|
+
:class="{
|
|
7
|
+
'kviewer-form-checkbox--circle': props.field.shapeType === 'circle',
|
|
8
|
+
'kviewer-form-checkbox--checked': isChecked
|
|
9
|
+
}"
|
|
10
|
+
role="checkbox"
|
|
11
|
+
:aria-checked="isChecked"
|
|
12
|
+
:aria-disabled="isReadOnly"
|
|
13
|
+
tabindex="0"
|
|
14
|
+
@click="toggle"
|
|
15
|
+
@keydown.space.prevent="toggle"
|
|
16
|
+
@keydown.enter.prevent="toggle"
|
|
17
|
+
>
|
|
18
|
+
<svg
|
|
19
|
+
v-if="isChecked"
|
|
20
|
+
class="kviewer-form-checkbox__icon"
|
|
21
|
+
viewBox="0 0 16 16"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
stroke-width="2.5"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
>
|
|
27
|
+
<line x1="3" y1="3" x2="13" y2="13" />
|
|
28
|
+
<line x1="13" y1="3" x2="3" y2="13" />
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Real PDF form field: native checkbox -->
|
|
33
|
+
<div v-else class="kviewer-form-checkbox">
|
|
3
34
|
<input
|
|
4
35
|
type="checkbox"
|
|
5
36
|
:checked="isChecked"
|
|
@@ -19,11 +50,16 @@ const props = defineProps({
|
|
|
19
50
|
});
|
|
20
51
|
const formFields = useFormFields();
|
|
21
52
|
const state = useViewerState();
|
|
53
|
+
const isDetected = computed(() => props.field.shapeType != null);
|
|
22
54
|
const isReadOnly = computed(() => state.readonly.value || props.field.readOnly);
|
|
23
55
|
const isChecked = computed(() => {
|
|
24
56
|
const fv = formFields.getFieldValue(props.field.id);
|
|
25
57
|
return fv?.value === true;
|
|
26
58
|
});
|
|
59
|
+
function toggle() {
|
|
60
|
+
if (isReadOnly.value) return;
|
|
61
|
+
formFields.setFieldValue(props.field.id, !isChecked.value);
|
|
62
|
+
}
|
|
27
63
|
function onChange(event) {
|
|
28
64
|
const target = event.target;
|
|
29
65
|
formFields.setFieldValue(props.field.id, target.checked);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
variant="ghost"
|
|
6
6
|
color="neutral"
|
|
7
7
|
size="sm"
|
|
8
|
+
data-testid="action-undo"
|
|
8
9
|
:disabled="!state.history.canUndo.value"
|
|
9
10
|
@click="state.history.undo"
|
|
10
11
|
/>
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
variant="ghost"
|
|
14
15
|
color="neutral"
|
|
15
16
|
size="sm"
|
|
17
|
+
data-testid="action-redo"
|
|
16
18
|
:disabled="!state.history.canRedo.value"
|
|
17
19
|
@click="state.history.redo"
|
|
18
20
|
/>
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
:variant="state.activeTool.value === 'eraser' ? 'soft' : 'ghost'"
|
|
22
24
|
color="neutral"
|
|
23
25
|
size="sm"
|
|
26
|
+
data-testid="action-eraser"
|
|
24
27
|
@click="state.selectTool('eraser')"
|
|
25
28
|
/>
|
|
26
29
|
</div>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
color="neutral"
|
|
7
7
|
size="xs"
|
|
8
8
|
title="Search"
|
|
9
|
+
data-testid="search-toggle"
|
|
9
10
|
@click="toggleOpen"
|
|
10
11
|
/>
|
|
11
12
|
|
|
@@ -21,11 +22,12 @@
|
|
|
21
22
|
color="neutral"
|
|
22
23
|
size="xs"
|
|
23
24
|
class="w-56"
|
|
25
|
+
data-testid="search-input"
|
|
24
26
|
@update:model-value="onQueryChange"
|
|
25
27
|
@keydown="onInputKeydown"
|
|
26
28
|
/>
|
|
27
29
|
|
|
28
|
-
<span class="min-w-16 text-center text-[11px] text-muted">
|
|
30
|
+
<span class="min-w-16 text-center text-[11px] text-muted" data-testid="search-status">
|
|
29
31
|
{{ statusLabel }}
|
|
30
32
|
</span>
|
|
31
33
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
color="neutral"
|
|
8
8
|
size="xs"
|
|
9
9
|
class="w-24"
|
|
10
|
+
data-testid="zoom-select"
|
|
10
11
|
@update:model-value="onZoomChange"
|
|
11
12
|
/>
|
|
12
13
|
<UButton
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
variant="ghost"
|
|
15
16
|
color="neutral"
|
|
16
17
|
size="xs"
|
|
18
|
+
data-testid="zoom-out"
|
|
17
19
|
@click="state.zoomOut"
|
|
18
20
|
/>
|
|
19
21
|
<UButton
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
variant="ghost"
|
|
22
24
|
color="neutral"
|
|
23
25
|
size="xs"
|
|
26
|
+
data-testid="zoom-in"
|
|
24
27
|
@click="state.zoomIn"
|
|
25
28
|
/>
|
|
26
29
|
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PageViewport } from 'pdfjs-dist';
|
|
2
|
+
import type { DetectedShape, DetectedShapeType } from '../annotation/engine/types.js';
|
|
3
|
+
export interface ViewportRect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
w: number;
|
|
9
|
+
h: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Convert a PDF-space bounding box to viewport-space rect.
|
|
13
|
+
* PDF origin is bottom-left; viewport origin is top-left.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pdfRectToViewport(viewport: PageViewport, x1: number, y1: number, x2: number, y2: number): ViewportRect;
|
|
16
|
+
/**
|
|
17
|
+
* True if a viewport-space rect looks like a checkbox / radio button.
|
|
18
|
+
* Size: 15–120 px on each axis. Aspect ratio: 0.6–1.4 (roughly square).
|
|
19
|
+
*/
|
|
20
|
+
export declare function isCheckboxLikeViewport(w: number, h: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* True if PDF-space dimensions look checkbox-sized (5–40 pt, roughly square).
|
|
23
|
+
* Used for the operator-stream scan before coordinate conversion.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isCheckboxLikePdf(w: number, h: number): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Widget-specific size check (Layer 1).
|
|
28
|
+
*
|
|
29
|
+
* For actual PDF form fields (Widget annotations) we KNOW the shape is a
|
|
30
|
+
* real interactive element, so we use a more lenient minimum size (8 px
|
|
31
|
+
* instead of 15 px) to avoid rejecting small checkboxes at lower zoom levels.
|
|
32
|
+
* Wide text-input widgets are still excluded via the aspect-ratio guard.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isCheckboxLikeWidget(w: number, h: number): boolean;
|
|
35
|
+
/** Build a DetectedShape record from viewport-space left / top / w / h. */
|
|
36
|
+
export declare function makeShape(id: string, left: number, top: number, w: number, h: number, shapeType?: DetectedShapeType): DetectedShape;
|
|
37
|
+
/** Characters that indicate checkbox / radio icon-font glyphs. */
|
|
38
|
+
export declare const SYMBOL_CHARS: Set<string>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function pdfRectToViewport(viewport, x1, y1, x2, y2) {
|
|
2
|
+
const [vx1, vy1] = viewport.convertToViewportPoint(x1, y1);
|
|
3
|
+
const [vx2, vy2] = viewport.convertToViewportPoint(x2, y2);
|
|
4
|
+
const left = Math.min(vx1, vx2);
|
|
5
|
+
const top = Math.min(vy1, vy2);
|
|
6
|
+
const right = Math.max(vx1, vx2);
|
|
7
|
+
const bottom = Math.max(vy1, vy2);
|
|
8
|
+
return { left, top, right, bottom, w: right - left, h: bottom - top };
|
|
9
|
+
}
|
|
10
|
+
export function isCheckboxLikeViewport(w, h) {
|
|
11
|
+
if (w < 15 || w > 120 || h < 15 || h > 120) return false;
|
|
12
|
+
const ratio = w / h;
|
|
13
|
+
return ratio > 0.6 && ratio < 1.4;
|
|
14
|
+
}
|
|
15
|
+
export function isCheckboxLikePdf(w, h) {
|
|
16
|
+
const aw = Math.abs(w);
|
|
17
|
+
const ah = Math.abs(h);
|
|
18
|
+
if (aw < 5 || aw > 40 || ah < 5 || ah > 40) return false;
|
|
19
|
+
const ratio = aw / ah;
|
|
20
|
+
return ratio >= 0.8 && ratio <= 1.25;
|
|
21
|
+
}
|
|
22
|
+
export function isCheckboxLikeWidget(w, h) {
|
|
23
|
+
if (w < 8 || w > 200 || h < 8 || h > 200) return false;
|
|
24
|
+
const ratio = w / h;
|
|
25
|
+
return ratio > 0.4 && ratio < 2.5;
|
|
26
|
+
}
|
|
27
|
+
export function makeShape(id, left, top, w, h, shapeType) {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
rect: { x: left, y: top, w, h },
|
|
31
|
+
center: { x: left + w / 2, y: top + h / 2 },
|
|
32
|
+
size: Math.max(w, h),
|
|
33
|
+
shapeType
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export const SYMBOL_CHARS = /* @__PURE__ */ new Set([
|
|
37
|
+
"\u25CB",
|
|
38
|
+
"\u25CF",
|
|
39
|
+
"\u25EF",
|
|
40
|
+
"\u25C9",
|
|
41
|
+
"\u2299",
|
|
42
|
+
"\u25CE",
|
|
43
|
+
"\u2B1C",
|
|
44
|
+
"\u2B1B",
|
|
45
|
+
"\u2610",
|
|
46
|
+
"\u2611",
|
|
47
|
+
"\u2612",
|
|
48
|
+
"\u25FB",
|
|
49
|
+
"\u25FC"
|
|
50
|
+
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ShallowRef } from 'vue';
|
|
2
|
-
import type { FormFieldDefinition, FormFieldValue } from '../annotation/engine/types.js';
|
|
2
|
+
import type { DetectedShape, FormFieldDefinition, FormFieldValue } from '../annotation/engine/types.js';
|
|
3
3
|
export interface FormFieldsState {
|
|
4
4
|
/** Field definitions grouped by page number */
|
|
5
5
|
fieldDefinitions: ShallowRef<Map<number, FormFieldDefinition[]>>;
|
|
@@ -19,6 +19,8 @@ export interface FormFieldsState {
|
|
|
19
19
|
setFieldValueByName: (fieldName: string, value: string | boolean | string[]) => void;
|
|
20
20
|
/** Reset all field values to their defaults (keeps definitions) */
|
|
21
21
|
resetValues: () => void;
|
|
22
|
+
/** Create synthetic checkbox form fields from detected shapes */
|
|
23
|
+
registerDetectedCheckboxes: (pageNumber: number, shapes: DetectedShape[], scale: number, pageHeight: number) => void;
|
|
22
24
|
/** Clear all values and definitions (for document change) */
|
|
23
25
|
reset: () => void;
|
|
24
26
|
}
|
|
@@ -69,6 +69,52 @@ export function provideFormFields() {
|
|
|
69
69
|
}
|
|
70
70
|
return "";
|
|
71
71
|
}
|
|
72
|
+
function registerDetectedCheckboxes(pageNumber, shapes, scale, pageHeight) {
|
|
73
|
+
const existingDefs = fieldDefinitions.value.get(pageNumber) ?? [];
|
|
74
|
+
const existingCenters = existingDefs.map((d) => ({
|
|
75
|
+
cx: (d.rect[0] + d.rect[2]) / 2,
|
|
76
|
+
cy: (d.rect[1] + d.rect[3]) / 2,
|
|
77
|
+
hw: (d.rect[2] - d.rect[0]) / 2,
|
|
78
|
+
hh: (d.rect[3] - d.rect[1]) / 2
|
|
79
|
+
}));
|
|
80
|
+
const newDefs = [];
|
|
81
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
82
|
+
const s = shapes[i];
|
|
83
|
+
const pdfX1 = s.rect.x / scale;
|
|
84
|
+
const pdfX2 = (s.rect.x + s.rect.w) / scale;
|
|
85
|
+
const pdfY1 = pageHeight - (s.rect.y + s.rect.h) / scale;
|
|
86
|
+
const pdfY2 = pageHeight - s.rect.y / scale;
|
|
87
|
+
const overlaps = existingCenters.some(
|
|
88
|
+
(ec) => ec.cx >= pdfX1 && ec.cx <= pdfX2 && ec.cy >= pdfY1 && ec.cy <= pdfY2
|
|
89
|
+
);
|
|
90
|
+
if (overlaps) continue;
|
|
91
|
+
const fieldId = `detected-chk-p${pageNumber}-${i}`;
|
|
92
|
+
newDefs.push({
|
|
93
|
+
id: fieldId,
|
|
94
|
+
pageNumber,
|
|
95
|
+
fieldType: "checkbox",
|
|
96
|
+
fieldName: `DetectedCheckbox_p${pageNumber}_${i}`,
|
|
97
|
+
rect: [pdfX1, pdfY1, pdfX2, pdfY2],
|
|
98
|
+
readOnly: false,
|
|
99
|
+
required: false,
|
|
100
|
+
exportValue: "Yes",
|
|
101
|
+
defaultValue: "Off",
|
|
102
|
+
shapeType: s.shapeType
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (newDefs.length === 0) return;
|
|
106
|
+
fieldDefinitions.value.set(pageNumber, [...existingDefs, ...newDefs]);
|
|
107
|
+
triggerRef(fieldDefinitions);
|
|
108
|
+
for (const def of newDefs) {
|
|
109
|
+
fieldValues.value.set(def.id, {
|
|
110
|
+
fieldId: def.id,
|
|
111
|
+
fieldName: def.fieldName,
|
|
112
|
+
fieldType: "checkbox",
|
|
113
|
+
value: false
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
triggerRef(fieldValues);
|
|
117
|
+
}
|
|
72
118
|
function resetValues() {
|
|
73
119
|
for (const [, defs] of fieldDefinitions.value.entries()) {
|
|
74
120
|
for (const def of defs) {
|
|
@@ -95,6 +141,7 @@ export function provideFormFields() {
|
|
|
95
141
|
getFieldsForPage,
|
|
96
142
|
getAllFieldValues,
|
|
97
143
|
setFieldValueByName,
|
|
144
|
+
registerDetectedCheckboxes,
|
|
98
145
|
resetValues,
|
|
99
146
|
reset
|
|
100
147
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape Detection Composable
|
|
3
|
+
*
|
|
4
|
+
* Detects checkbox-like shapes on each PDF page using three layers:
|
|
5
|
+
* 1. PDF annotations (Widget, Btn, Square, Circle form fields)
|
|
6
|
+
* 2. Graphical shapes from the operator stream (vector rectangles / circles)
|
|
7
|
+
* 3. Icon-font glyphs from the text layer (Unicode symbols, Private Use Area)
|
|
8
|
+
*
|
|
9
|
+
* Used by the auto-stamp feature to snap stamps onto existing form checkboxes.
|
|
10
|
+
*/
|
|
11
|
+
import type { PDFPageProxy, PageViewport } from 'pdfjs-dist';
|
|
12
|
+
import type { DetectedShape } from '../annotation/engine/types.js';
|
|
13
|
+
export interface ShapeDetection {
|
|
14
|
+
/** Run all three detection layers on a page and cache the results. */
|
|
15
|
+
preprocessShapesForPage: (pageNumber: number, pdfPage: PDFPageProxy, viewport: PageViewport) => Promise<DetectedShape[]>;
|
|
16
|
+
/** Fast O(n) lookup: first shape whose rect contains (x, y). */
|
|
17
|
+
findShapeAtPoint: (pageNumber: number, x: number, y: number) => DetectedShape | null;
|
|
18
|
+
/** Clear cached shapes for one page (e.g. after a zoom change). */
|
|
19
|
+
clearShapeCache: (pageNumber: number) => void;
|
|
20
|
+
/** Clear the entire shape cache (e.g. when a new PDF is loaded). */
|
|
21
|
+
clearAllShapeCache: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createShapeDetection(): ShapeDetection;
|
|
24
|
+
export declare function useShapeDetection(): ShapeDetection;
|