kviewer 0.1.2 → 0.2.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/README.md +28 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -4
- package/dist/runtime/annotation/engine/config.d.ts +1 -1
- package/dist/runtime/annotation/engine/config.js +2 -2
- package/dist/runtime/annotation/engine/painter.d.ts +1 -1
- package/dist/runtime/annotation/engine/utils.d.ts +2 -1
- package/dist/runtime/annotation/engine/utils.js +3 -7
- package/dist/runtime/annotation/pdf-export/export.js +63 -9
- package/dist/runtime/annotation/pdf-export/parse_circle.js +3 -2
- package/dist/runtime/annotation/pdf-export/parse_freetext.js +18 -8
- package/dist/runtime/annotation/pdf-export/parse_highlight.js +27 -29
- package/dist/runtime/annotation/pdf-export/parse_ink.js +5 -3
- package/dist/runtime/annotation/pdf-export/parse_line.js +5 -3
- package/dist/runtime/annotation/pdf-export/parse_polyline.js +6 -4
- package/dist/runtime/annotation/pdf-export/parse_square.js +3 -2
- package/dist/runtime/annotation/pdf-export/parse_stamp.js +16 -9
- package/dist/runtime/annotation/pdf-export/parse_strikeout.js +11 -10
- package/dist/runtime/annotation/pdf-export/parse_text.js +3 -1
- package/dist/runtime/annotation/pdf-export/parse_underline.js +11 -10
- package/dist/runtime/annotation/pdf-export/parse_utils.d.ts +11 -0
- package/dist/runtime/annotation/pdf-export/parse_utils.js +12 -0
- package/dist/runtime/annotation/pdf-import/decode.d.ts +2 -1
- package/dist/runtime/annotation/pdf-import/decode.js +6 -5
- package/dist/runtime/annotation/pdf-import/decode_circle.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_freetext.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_highlight.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_ink.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_line.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_square.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_stamp.js +1 -1
- package/dist/runtime/annotation/pdf-import/decode_text.js +1 -1
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +3 -2
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +7 -15
- package/dist/runtime/annotation/pdf-import/transform.d.ts +85 -0
- package/dist/runtime/annotation/pdf-import/transform.js +81 -0
- package/dist/runtime/annotation/pdf-import/types.d.ts +5 -1
- package/dist/runtime/annotation/pdf-import/utils.d.ts +6 -5
- package/dist/runtime/annotation/pdf-import/utils.js +53 -38
- package/dist/runtime/components/AnnotationToolbar.vue +16 -0
- package/dist/runtime/components/FormFieldLayer.d.vue.ts +4 -7
- package/dist/runtime/components/FormFieldLayer.vue +5 -10
- package/dist/runtime/components/FormFieldLayer.vue.d.ts +4 -7
- package/dist/runtime/components/PdfPage.vue +1 -6
- package/dist/runtime/components/Viewer.d.vue.ts +12 -0
- package/dist/runtime/components/Viewer.vue +35 -2
- package/dist/runtime/components/Viewer.vue.d.ts +12 -0
- package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +5 -6
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +8 -14
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +5 -6
- package/dist/runtime/composables/usePageVirtualization.d.ts +17 -0
- package/dist/runtime/composables/usePageVirtualization.js +48 -2
- package/dist/runtime/embed/bridge-client.d.ts +9 -1
- package/dist/runtime/embed/bridge-client.js +13 -0
- package/dist/runtime/embed/bridge-host.d.ts +3 -0
- package/dist/runtime/embed/bridge-host.js +30 -8
- package/dist/runtime/embed/protocol.d.ts +21 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Konva from 'konva';
|
|
2
2
|
import { type AnnotationType, type IAnnotationComment, type IAnnotationStore, type PdfjsAnnotationSubtype } from '../engine/types.js';
|
|
3
3
|
import { type RawPdfAnnotation } from './types.js';
|
|
4
|
+
import { type PageTransform } from './transform.js';
|
|
4
5
|
type Rect = {
|
|
5
6
|
x: number;
|
|
6
7
|
y: number;
|
|
@@ -17,8 +18,8 @@ export declare function getAnnotationText(annotation: RawPdfAnnotation): string;
|
|
|
17
18
|
export declare function getFreeTextContent(annotation: RawPdfAnnotation): string;
|
|
18
19
|
export declare function getAnnotationDate(annotation: RawPdfAnnotation): string;
|
|
19
20
|
export declare function getComments(annotation: RawPdfAnnotation, allAnnotations: RawPdfAnnotation[]): IAnnotationComment[];
|
|
20
|
-
export declare function rectFromPdfRect(input: unknown,
|
|
21
|
-
export declare function pointFromPdfPoint(input: unknown,
|
|
21
|
+
export declare function rectFromPdfRect(input: unknown, transform: PageTransform): Rect | null;
|
|
22
|
+
export declare function pointFromPdfPoint(input: unknown, transform: PageTransform): {
|
|
22
23
|
x: number;
|
|
23
24
|
y: number;
|
|
24
25
|
} | null;
|
|
@@ -26,9 +27,9 @@ export declare function localPointFromPdfPoint(input: unknown): {
|
|
|
26
27
|
x: number;
|
|
27
28
|
y: number;
|
|
28
29
|
} | null;
|
|
29
|
-
export declare function rectFromQuadPoints(input: unknown,
|
|
30
|
-
export declare function pointsFromInkList(input: unknown,
|
|
31
|
-
export declare function linePointsFromCoordinates(input: unknown,
|
|
30
|
+
export declare function rectFromQuadPoints(input: unknown, transform: PageTransform): Rect | null;
|
|
31
|
+
export declare function pointsFromInkList(input: unknown, transform: PageTransform): number[];
|
|
32
|
+
export declare function linePointsFromCoordinates(input: unknown, transform: PageTransform): number[] | null;
|
|
32
33
|
export declare function normalizedStrokeWidth(annotation: RawPdfAnnotation, fallback?: number, options?: StrokeWidthOptions): number;
|
|
33
34
|
export declare function dashedBorder(annotation: RawPdfAnnotation): number[];
|
|
34
35
|
export declare function hasArrowEnding(value: unknown): boolean;
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
isRawPdfAnnotation,
|
|
15
15
|
isString
|
|
16
16
|
} from "./types.js";
|
|
17
|
+
import {
|
|
18
|
+
convertPdfPointToCanvas,
|
|
19
|
+
convertPdfRectToCanvas
|
|
20
|
+
} from "./transform.js";
|
|
17
21
|
const UNKNOWN_USER = "Unknown user";
|
|
18
22
|
function clampColor(value) {
|
|
19
23
|
if (!Number.isFinite(value)) return 0;
|
|
@@ -108,23 +112,18 @@ export function getComments(annotation, allAnnotations) {
|
|
|
108
112
|
}
|
|
109
113
|
return comments;
|
|
110
114
|
}
|
|
111
|
-
export function rectFromPdfRect(input,
|
|
115
|
+
export function rectFromPdfRect(input, transform) {
|
|
112
116
|
if (!Array.isArray(input) || input.length < 4) return null;
|
|
113
117
|
const [x1, y1, x2, y2] = input;
|
|
114
118
|
if (!isNumber(x1) || !isNumber(y1) || !isNumber(x2) || !isNumber(y2)) return null;
|
|
115
|
-
return
|
|
116
|
-
x: x1,
|
|
117
|
-
y: pageHeight - y2,
|
|
118
|
-
width: x2 - x1,
|
|
119
|
-
height: y2 - y1
|
|
120
|
-
};
|
|
119
|
+
return convertPdfRectToCanvas(transform, [x1, y1, x2, y2]);
|
|
121
120
|
}
|
|
122
|
-
export function pointFromPdfPoint(input,
|
|
121
|
+
export function pointFromPdfPoint(input, transform) {
|
|
123
122
|
const pair = toNumericTuple(input, 2);
|
|
124
|
-
if (pair) return
|
|
123
|
+
if (pair) return convertPdfPointToCanvas(transform, pair[0], pair[1]);
|
|
125
124
|
const point = asPoint(input);
|
|
126
125
|
if (!point || !isNumber(point.x) || !isNumber(point.y)) return null;
|
|
127
|
-
return
|
|
126
|
+
return convertPdfPointToCanvas(transform, point.x, point.y);
|
|
128
127
|
}
|
|
129
128
|
export function localPointFromPdfPoint(input) {
|
|
130
129
|
const pair = toNumericTuple(input, 2);
|
|
@@ -133,55 +132,71 @@ export function localPointFromPdfPoint(input) {
|
|
|
133
132
|
if (!point || !isNumber(point.x) || !isNumber(point.y)) return null;
|
|
134
133
|
return { x: point.x, y: point.y };
|
|
135
134
|
}
|
|
136
|
-
|
|
135
|
+
function rawPdfPoint(input) {
|
|
136
|
+
const pair = toNumericTuple(input, 2);
|
|
137
|
+
if (pair) return [pair[0], pair[1]];
|
|
138
|
+
const point = asPoint(input);
|
|
139
|
+
if (point && isNumber(point.x) && isNumber(point.y)) return [point.x, point.y];
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
export function rectFromQuadPoints(input, transform) {
|
|
143
|
+
let raw = null;
|
|
137
144
|
const numeric = toCoordinateList(input);
|
|
138
145
|
if (numeric && numeric.length >= 8) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
raw = [
|
|
147
|
+
[numeric[0], numeric[1]],
|
|
148
|
+
[numeric[2], numeric[3]],
|
|
149
|
+
[numeric[4], numeric[5]],
|
|
150
|
+
[numeric[6], numeric[7]]
|
|
151
|
+
];
|
|
152
|
+
} else if (Array.isArray(input) && input.length >= 4) {
|
|
153
|
+
const collected = [];
|
|
154
|
+
for (let i = 0; i < 4; i++) {
|
|
155
|
+
const p = rawPdfPoint(input[i]);
|
|
156
|
+
if (!p) return null;
|
|
157
|
+
collected.push(p);
|
|
158
|
+
}
|
|
159
|
+
raw = collected;
|
|
149
160
|
}
|
|
150
|
-
if (!
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
if (!raw) return null;
|
|
162
|
+
const xs = [];
|
|
163
|
+
const ys = [];
|
|
164
|
+
for (const [rx, ry] of raw) {
|
|
165
|
+
const p = convertPdfPointToCanvas(transform, rx, ry);
|
|
166
|
+
xs.push(p.x);
|
|
167
|
+
ys.push(p.y);
|
|
168
|
+
}
|
|
169
|
+
const minX = Math.min(...xs);
|
|
170
|
+
const maxX = Math.max(...xs);
|
|
171
|
+
const minY = Math.min(...ys);
|
|
172
|
+
const maxY = Math.max(...ys);
|
|
173
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
161
174
|
}
|
|
162
|
-
export function pointsFromInkList(input,
|
|
175
|
+
export function pointsFromInkList(input, transform) {
|
|
163
176
|
const numeric = toCoordinateList(input);
|
|
164
177
|
if (numeric) {
|
|
165
178
|
const points2 = [];
|
|
166
179
|
for (let index = 0; index + 1 < numeric.length; index += 2) {
|
|
167
|
-
|
|
180
|
+
const p = convertPdfPointToCanvas(transform, numeric[index], numeric[index + 1]);
|
|
181
|
+
points2.push(p.x, p.y);
|
|
168
182
|
}
|
|
169
183
|
return points2;
|
|
170
184
|
}
|
|
171
185
|
if (!Array.isArray(input)) return [];
|
|
172
186
|
const points = [];
|
|
173
187
|
for (const item of input) {
|
|
174
|
-
const converted = pointFromPdfPoint(item,
|
|
188
|
+
const converted = pointFromPdfPoint(item, transform);
|
|
175
189
|
if (!converted) continue;
|
|
176
190
|
points.push(converted.x, converted.y);
|
|
177
191
|
}
|
|
178
192
|
return points;
|
|
179
193
|
}
|
|
180
|
-
export function linePointsFromCoordinates(input,
|
|
194
|
+
export function linePointsFromCoordinates(input, transform) {
|
|
181
195
|
const tuple = toNumericTuple(input, 4);
|
|
182
196
|
if (!tuple) return null;
|
|
183
|
-
const
|
|
184
|
-
|
|
197
|
+
const a = convertPdfPointToCanvas(transform, tuple[0], tuple[1]);
|
|
198
|
+
const b = convertPdfPointToCanvas(transform, tuple[2], tuple[3]);
|
|
199
|
+
return [a.x, a.y, b.x, b.y];
|
|
185
200
|
}
|
|
186
201
|
export function normalizedStrokeWidth(annotation, fallback = 1, options = {}) {
|
|
187
202
|
const borderStyle = asBorderStyle(annotation.borderStyle);
|
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
{{ state.selectedAnnotations.value.length }} selected
|
|
45
45
|
</span>
|
|
46
46
|
|
|
47
|
+
<!-- Edit text (free text only) -->
|
|
48
|
+
<button
|
|
49
|
+
v-if="isFreeText"
|
|
50
|
+
class="flex items-center justify-center w-7 h-7 rounded-md hover:bg-default cursor-pointer"
|
|
51
|
+
title="Edit text"
|
|
52
|
+
data-testid="annotation-edit-text"
|
|
53
|
+
@click="onEditText"
|
|
54
|
+
>
|
|
55
|
+
<UIcon name="i-lucide-pencil" class="text-sm text-muted" />
|
|
56
|
+
</button>
|
|
57
|
+
|
|
47
58
|
<!-- Delete -->
|
|
48
59
|
<button
|
|
49
60
|
class="flex items-center justify-center w-7 h-7 rounded-md hover:bg-default cursor-pointer"
|
|
@@ -114,6 +125,11 @@ function onFontSizeChange(value) {
|
|
|
114
125
|
ann.fontSize = fontSize;
|
|
115
126
|
state.selectedAnnotation.value = { ...ann };
|
|
116
127
|
}
|
|
128
|
+
function onEditText() {
|
|
129
|
+
const ann = state.selectedAnnotation.value;
|
|
130
|
+
if (!ann || !state.painter.value) return;
|
|
131
|
+
void state.painter.value.editFreeText(ann.id);
|
|
132
|
+
}
|
|
117
133
|
function onDelete() {
|
|
118
134
|
if (isMultiSelect.value) {
|
|
119
135
|
const ids = state.selectedAnnotations.value.map((a) => a.id);
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
+
import type { PageTransform } from '../annotation/pdf-import/transform.js';
|
|
1
2
|
type __VLS_Props = {
|
|
2
3
|
pageNumber: number;
|
|
3
4
|
scale: number;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** Carries displayed width/height plus the page's `/Rotate` and MediaBox
|
|
6
|
+
* origin — the single source of truth for mapping `/Rect` to canvas. */
|
|
7
|
+
pageTransform: PageTransform;
|
|
6
8
|
pointerEvents: string;
|
|
7
9
|
zIndex: number;
|
|
8
|
-
/** PDF user-space origin offsets (`view[0]`, `view[1]`). Forwarded to
|
|
9
|
-
* FormFieldWrapper so /Rect coordinates can be mapped to canvas space
|
|
10
|
-
* when the page's MediaBox is not at (0, 0). */
|
|
11
|
-
viewOffsetX?: number;
|
|
12
|
-
viewOffsetY?: number;
|
|
13
10
|
};
|
|
14
11
|
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>;
|
|
15
12
|
declare const _default: typeof __VLS_export;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
position: 'absolute',
|
|
7
7
|
top: 0,
|
|
8
8
|
left: 0,
|
|
9
|
-
width: props.
|
|
10
|
-
height: props.
|
|
9
|
+
width: props.pageTransform.width + 'px',
|
|
10
|
+
height: props.pageTransform.height + 'px',
|
|
11
11
|
transform: `scale(${props.scale})`,
|
|
12
12
|
transformOrigin: '0 0',
|
|
13
13
|
zIndex: props.zIndex,
|
|
@@ -22,10 +22,8 @@
|
|
|
22
22
|
v-for="field in fields"
|
|
23
23
|
:key="field.id"
|
|
24
24
|
:field="field"
|
|
25
|
-
:page-
|
|
25
|
+
:page-transform="props.pageTransform"
|
|
26
26
|
:scale="props.scale"
|
|
27
|
-
:view-offset-x="props.viewOffsetX ?? 0"
|
|
28
|
-
:view-offset-y="props.viewOffsetY ?? 0"
|
|
29
27
|
:selected="selectedFieldId === field.id"
|
|
30
28
|
@select="formFields.selectPlacedField(field.id)"
|
|
31
29
|
/>
|
|
@@ -41,12 +39,9 @@ import FormFieldWrapper from "./form-fields/FormFieldWrapper.vue";
|
|
|
41
39
|
const props = defineProps({
|
|
42
40
|
pageNumber: { type: Number, required: true },
|
|
43
41
|
scale: { type: Number, required: true },
|
|
44
|
-
|
|
45
|
-
pageHeight: { type: Number, required: true },
|
|
42
|
+
pageTransform: { type: Object, required: true },
|
|
46
43
|
pointerEvents: { type: String, required: true },
|
|
47
|
-
zIndex: { type: Number, required: true }
|
|
48
|
-
viewOffsetX: { type: Number, required: false },
|
|
49
|
-
viewOffsetY: { type: Number, required: false }
|
|
44
|
+
zIndex: { type: Number, required: true }
|
|
50
45
|
});
|
|
51
46
|
const formFields = useFormFields();
|
|
52
47
|
const state = useViewerState();
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
+
import type { PageTransform } from '../annotation/pdf-import/transform.js';
|
|
1
2
|
type __VLS_Props = {
|
|
2
3
|
pageNumber: number;
|
|
3
4
|
scale: number;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** Carries displayed width/height plus the page's `/Rotate` and MediaBox
|
|
6
|
+
* origin — the single source of truth for mapping `/Rect` to canvas. */
|
|
7
|
+
pageTransform: PageTransform;
|
|
6
8
|
pointerEvents: string;
|
|
7
9
|
zIndex: number;
|
|
8
|
-
/** PDF user-space origin offsets (`view[0]`, `view[1]`). Forwarded to
|
|
9
|
-
* FormFieldWrapper so /Rect coordinates can be mapped to canvas space
|
|
10
|
-
* when the page's MediaBox is not at (0, 0). */
|
|
11
|
-
viewOffsetX?: number;
|
|
12
|
-
viewOffsetY?: number;
|
|
13
10
|
};
|
|
14
11
|
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>;
|
|
15
12
|
declare const _default: typeof __VLS_export;
|
|
@@ -29,10 +29,7 @@
|
|
|
29
29
|
<FormFieldLayer
|
|
30
30
|
:page-number="props.pageNumber"
|
|
31
31
|
:scale="props.scale"
|
|
32
|
-
:page-
|
|
33
|
-
:page-height="baseHeight"
|
|
34
|
-
:view-offset-x="viewOffsetX"
|
|
35
|
-
:view-offset-y="viewOffsetY"
|
|
32
|
+
:page-transform="props.pageMeta"
|
|
36
33
|
:pointer-events="formFieldPointerEvents"
|
|
37
34
|
:z-index="formFieldZIndex"
|
|
38
35
|
/>
|
|
@@ -86,8 +83,6 @@ let currentRenderTask = null;
|
|
|
86
83
|
let textLayer = null;
|
|
87
84
|
const baseWidth = props.pageMeta.width;
|
|
88
85
|
const baseHeight = props.pageMeta.height;
|
|
89
|
-
const viewOffsetX = props.pageMeta.viewOffsetX ?? 0;
|
|
90
|
-
const viewOffsetY = props.pageMeta.viewOffsetY ?? 0;
|
|
91
86
|
const cssWidth = computed(() => baseWidth * props.scale);
|
|
92
87
|
const cssHeight = computed(() => baseHeight * props.scale);
|
|
93
88
|
const isAnnotating = computed(() => {
|
|
@@ -84,12 +84,24 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
84
84
|
setFormEditMode: (enabled: boolean) => void;
|
|
85
85
|
/** Flip form-edit mode. Returns the new state. */
|
|
86
86
|
toggleFormEditMode: () => boolean;
|
|
87
|
+
/** True once the user has viewed every page — each page has entered the
|
|
88
|
+
* viewport at least once. Use to gate a "must read all pages" sign action.
|
|
89
|
+
* See also the `all-pages-read` event for a push-style signal. */
|
|
90
|
+
allPagesRead: () => boolean;
|
|
91
|
+
/** Page numbers viewed so far, ascending. */
|
|
92
|
+
getViewedPages: () => number[];
|
|
93
|
+
/** Clear viewed-page tracking, e.g. to restart a signing session. */
|
|
94
|
+
resetViewedPages: () => void;
|
|
87
95
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
96
|
+
"all-pages-read": () => any;
|
|
88
97
|
"update:formEditMode": (value: boolean) => any;
|
|
89
98
|
"update:activeRoleId": (value: string | null) => any;
|
|
99
|
+
"update:viewedPages": (pages: number[]) => any;
|
|
90
100
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
101
|
+
"onAll-pages-read"?: (() => any) | undefined;
|
|
91
102
|
"onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
|
|
92
103
|
"onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
|
|
104
|
+
"onUpdate:viewedPages"?: ((pages: number[]) => any) | undefined;
|
|
93
105
|
}>, {
|
|
94
106
|
userName: string;
|
|
95
107
|
freehandGroupingDelay: number;
|
|
@@ -229,7 +229,7 @@ const props = defineProps({
|
|
|
229
229
|
scripting: { type: Boolean, required: false, default: false },
|
|
230
230
|
menuItems: { type: Array, required: false, default: void 0 }
|
|
231
231
|
});
|
|
232
|
-
const emit = defineEmits(["update:formEditMode", "update:activeRoleId"]);
|
|
232
|
+
const emit = defineEmits(["update:formEditMode", "update:activeRoleId", "update:viewedPages", "all-pages-read"]);
|
|
233
233
|
const viewerRoot = ref(null);
|
|
234
234
|
const scrollContainer = ref(null);
|
|
235
235
|
const { state: viewerState, setScrollToPageFn, setDownloadPdfFn } = provideViewerState();
|
|
@@ -306,6 +306,31 @@ watch(
|
|
|
306
306
|
viewerState.currentPage.value = page;
|
|
307
307
|
}
|
|
308
308
|
);
|
|
309
|
+
watch(
|
|
310
|
+
() => viewerState.currentPage.value,
|
|
311
|
+
(page) => virtualization.markPageViewed(page),
|
|
312
|
+
{ immediate: true }
|
|
313
|
+
);
|
|
314
|
+
watch(
|
|
315
|
+
() => pageMetas.value.length,
|
|
316
|
+
(len) => {
|
|
317
|
+
if (len > 0 && !isSinglePageMode.value) {
|
|
318
|
+
nextTick(() => virtualization.updateCurrentPageByRect());
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
watch(
|
|
323
|
+
virtualization.allPagesRead,
|
|
324
|
+
(read, prev) => {
|
|
325
|
+
if (read && !prev) emit("all-pages-read");
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
watch(
|
|
329
|
+
virtualization.viewedPages,
|
|
330
|
+
(pages) => {
|
|
331
|
+
emit("update:viewedPages", [...pages].sort((a, b) => a - b));
|
|
332
|
+
}
|
|
333
|
+
);
|
|
309
334
|
function onPageRef(pageNumber, el) {
|
|
310
335
|
if (el) {
|
|
311
336
|
virtualization.observePage(pageNumber, el);
|
|
@@ -892,7 +917,15 @@ defineExpose({
|
|
|
892
917
|
const next = !viewerState.formEditMode.value;
|
|
893
918
|
viewerState.setFormEditMode(next);
|
|
894
919
|
return next;
|
|
895
|
-
}
|
|
920
|
+
},
|
|
921
|
+
/** True once the user has viewed every page — each page has entered the
|
|
922
|
+
* viewport at least once. Use to gate a "must read all pages" sign action.
|
|
923
|
+
* See also the `all-pages-read` event for a push-style signal. */
|
|
924
|
+
allPagesRead: () => virtualization.allPagesRead.value,
|
|
925
|
+
/** Page numbers viewed so far, ascending. */
|
|
926
|
+
getViewedPages: () => [...virtualization.viewedPages.value].sort((a, b) => a - b),
|
|
927
|
+
/** Clear viewed-page tracking, e.g. to restart a signing session. */
|
|
928
|
+
resetViewedPages: () => virtualization.resetViewedPages()
|
|
896
929
|
});
|
|
897
930
|
</script>
|
|
898
931
|
|
|
@@ -84,12 +84,24 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
|
|
|
84
84
|
setFormEditMode: (enabled: boolean) => void;
|
|
85
85
|
/** Flip form-edit mode. Returns the new state. */
|
|
86
86
|
toggleFormEditMode: () => boolean;
|
|
87
|
+
/** True once the user has viewed every page — each page has entered the
|
|
88
|
+
* viewport at least once. Use to gate a "must read all pages" sign action.
|
|
89
|
+
* See also the `all-pages-read` event for a push-style signal. */
|
|
90
|
+
allPagesRead: () => boolean;
|
|
91
|
+
/** Page numbers viewed so far, ascending. */
|
|
92
|
+
getViewedPages: () => number[];
|
|
93
|
+
/** Clear viewed-page tracking, e.g. to restart a signing session. */
|
|
94
|
+
resetViewedPages: () => void;
|
|
87
95
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
96
|
+
"all-pages-read": () => any;
|
|
88
97
|
"update:formEditMode": (value: boolean) => any;
|
|
89
98
|
"update:activeRoleId": (value: string | null) => any;
|
|
99
|
+
"update:viewedPages": (pages: number[]) => any;
|
|
90
100
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
101
|
+
"onAll-pages-read"?: (() => any) | undefined;
|
|
91
102
|
"onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
|
|
92
103
|
"onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
|
|
104
|
+
"onUpdate:viewedPages"?: ((pages: number[]) => any) | undefined;
|
|
93
105
|
}>, {
|
|
94
106
|
userName: string;
|
|
95
107
|
freehandGroupingDelay: number;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { FormFieldDefinition } from '../../annotation/engine/types.js';
|
|
2
|
+
import { type PageTransform } from '../../annotation/pdf-import/transform.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
field: FormFieldDefinition;
|
|
4
|
-
|
|
5
|
+
/** Coord transform for this page. `/Rect` is in unrotated user space; the
|
|
6
|
+
* transform applies the page's `/Rotate` and the MediaBox origin so the
|
|
7
|
+
* field lands at the right canvas position regardless of page rotation. */
|
|
8
|
+
pageTransform: PageTransform;
|
|
5
9
|
scale: number;
|
|
6
|
-
/** PDF user-space origin (view[0], view[1]). /Rect coords are in
|
|
7
|
-
* default user space; the canvas top-left corresponds to PDF
|
|
8
|
-
* (viewOffsetX, viewOffsetY + pageHeight), so we have to subtract. */
|
|
9
|
-
viewOffsetX?: number;
|
|
10
|
-
viewOffsetY?: number;
|
|
11
10
|
selected: boolean;
|
|
12
11
|
};
|
|
13
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
:field="props.field"
|
|
27
27
|
:selected="props.selected"
|
|
28
28
|
:scale="props.scale"
|
|
29
|
-
:page-height="props.
|
|
29
|
+
:page-height="props.pageTransform.height"
|
|
30
30
|
@select="emit('select')"
|
|
31
31
|
/>
|
|
32
32
|
</div>
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
<script setup>
|
|
36
36
|
import { computed } from "vue";
|
|
37
|
+
import { convertPdfRectToCanvas } from "../../annotation/pdf-import/transform";
|
|
37
38
|
import { useViewerState } from "../../composables/useViewerState";
|
|
38
39
|
import { useFormFields } from "../../composables/useFormFields";
|
|
39
40
|
import FormTextField from "./FormTextField.vue";
|
|
@@ -45,10 +46,8 @@ import FormButton from "./FormButton.vue";
|
|
|
45
46
|
import PlacedFieldChrome from "./PlacedFieldChrome.vue";
|
|
46
47
|
const props = defineProps({
|
|
47
48
|
field: { type: Object, required: true },
|
|
48
|
-
|
|
49
|
+
pageTransform: { type: Object, required: true },
|
|
49
50
|
scale: { type: Number, required: true },
|
|
50
|
-
viewOffsetX: { type: Number, required: false },
|
|
51
|
-
viewOffsetY: { type: Number, required: false },
|
|
52
51
|
selected: { type: Boolean, required: true }
|
|
53
52
|
});
|
|
54
53
|
const emit = defineEmits(["select"]);
|
|
@@ -64,18 +63,13 @@ const chromeColor = computed(() => {
|
|
|
64
63
|
});
|
|
65
64
|
const positionStyle = computed(() => {
|
|
66
65
|
const rect = props.field.rect;
|
|
67
|
-
const
|
|
68
|
-
const vy = props.viewOffsetY ?? 0;
|
|
69
|
-
const left = rect[0] - vx;
|
|
70
|
-
const top = props.pageHeight + vy - rect[3];
|
|
71
|
-
const width = rect[2] - rect[0];
|
|
72
|
-
const height = rect[3] - rect[1];
|
|
66
|
+
const r = convertPdfRectToCanvas(props.pageTransform, [rect[0], rect[1], rect[2], rect[3]]);
|
|
73
67
|
return {
|
|
74
68
|
position: "absolute",
|
|
75
|
-
left: `${
|
|
76
|
-
top: `${
|
|
77
|
-
width: `${width}px`,
|
|
78
|
-
height: `${height}px`
|
|
69
|
+
left: `${r.x}px`,
|
|
70
|
+
top: `${r.y}px`,
|
|
71
|
+
width: `${r.width}px`,
|
|
72
|
+
height: `${r.height}px`
|
|
79
73
|
};
|
|
80
74
|
});
|
|
81
75
|
</script>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { FormFieldDefinition } from '../../annotation/engine/types.js';
|
|
2
|
+
import { type PageTransform } from '../../annotation/pdf-import/transform.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
field: FormFieldDefinition;
|
|
4
|
-
|
|
5
|
+
/** Coord transform for this page. `/Rect` is in unrotated user space; the
|
|
6
|
+
* transform applies the page's `/Rotate` and the MediaBox origin so the
|
|
7
|
+
* field lands at the right canvas position regardless of page rotation. */
|
|
8
|
+
pageTransform: PageTransform;
|
|
5
9
|
scale: number;
|
|
6
|
-
/** PDF user-space origin (view[0], view[1]). /Rect coords are in
|
|
7
|
-
* default user space; the canvas top-left corresponds to PDF
|
|
8
|
-
* (viewOffsetX, viewOffsetY + pageHeight), so we have to subtract. */
|
|
9
|
-
viewOffsetX?: number;
|
|
10
|
-
viewOffsetY?: number;
|
|
11
10
|
selected: boolean;
|
|
12
11
|
};
|
|
13
12
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
@@ -2,8 +2,14 @@ import { type ShallowRef, type ComputedRef, type Ref } from 'vue';
|
|
|
2
2
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
3
3
|
export interface PageMeta {
|
|
4
4
|
pageNumber: number;
|
|
5
|
+
/** Displayed page width — already accounts for the page's intrinsic
|
|
6
|
+
* `/Rotate`, so width/height are swapped for 90°/270° pages. */
|
|
5
7
|
width: number;
|
|
6
8
|
height: number;
|
|
9
|
+
/** The page's intrinsic `/Rotate` value, normalized to 0 | 90 | 180 | 270.
|
|
10
|
+
* `getViewport()` bakes this in by default; annotation layers that map
|
|
11
|
+
* unrotated `/Rect` coordinates need it to apply the same rotation. */
|
|
12
|
+
rotation: number;
|
|
7
13
|
/** PDF user-space origin of the page's visible area (`view[0]`, `view[1]`).
|
|
8
14
|
* Almost always (0, 0); non-zero for PDFs whose MediaBox has been
|
|
9
15
|
* shifted. Annotations in `/Rect` are in default user space, so any
|
|
@@ -15,6 +21,17 @@ export interface PageVirtualization {
|
|
|
15
21
|
pageMetas: ShallowRef<PageMeta[]>;
|
|
16
22
|
currentPage: Ref<number>;
|
|
17
23
|
renderedPages: ComputedRef<Set<number>>;
|
|
24
|
+
/** Pages that have entered the viewport at least once since the document
|
|
25
|
+
* loaded (or since the last `resetViewedPages()`). Marked from scroll/pan
|
|
26
|
+
* geometry — not render state — so a fast fling that skips rendering the
|
|
27
|
+
* middle pages still counts them as viewed. */
|
|
28
|
+
viewedPages: ShallowRef<Set<number>>;
|
|
29
|
+
/** True once every page has been viewed. */
|
|
30
|
+
allPagesRead: ComputedRef<boolean>;
|
|
31
|
+
/** Mark a single page as viewed (used by single-page navigation). */
|
|
32
|
+
markPageViewed: (pageNumber: number) => void;
|
|
33
|
+
/** Clear the viewed-pages set, e.g. to start a fresh signing session. */
|
|
34
|
+
resetViewedPages: () => void;
|
|
18
35
|
isPageRendered: (pageNumber: number) => boolean;
|
|
19
36
|
observePage: (pageNumber: number, element: HTMLElement) => void;
|
|
20
37
|
unobservePage: (pageNumber: number) => void;
|
|
@@ -5,6 +5,30 @@ export function createPageVirtualization() {
|
|
|
5
5
|
const pageMetas = shallowRef([]);
|
|
6
6
|
const currentPage = ref(1);
|
|
7
7
|
const visiblePages = shallowRef(/* @__PURE__ */ new Set());
|
|
8
|
+
const viewedPages = shallowRef(/* @__PURE__ */ new Set());
|
|
9
|
+
const allPagesRead = computed(() => {
|
|
10
|
+
const total = pageMetas.value.length;
|
|
11
|
+
return total > 0 && viewedPages.value.size >= total;
|
|
12
|
+
});
|
|
13
|
+
function markPageViewed(pageNumber) {
|
|
14
|
+
if (viewedPages.value.has(pageNumber)) return;
|
|
15
|
+
const next = new Set(viewedPages.value);
|
|
16
|
+
next.add(pageNumber);
|
|
17
|
+
viewedPages.value = next;
|
|
18
|
+
}
|
|
19
|
+
function markPagesViewed(pageNumbers) {
|
|
20
|
+
let next = null;
|
|
21
|
+
for (const n of pageNumbers) {
|
|
22
|
+
if (viewedPages.value.has(n) || next?.has(n)) continue;
|
|
23
|
+
if (!next) next = new Set(viewedPages.value);
|
|
24
|
+
next.add(n);
|
|
25
|
+
}
|
|
26
|
+
if (next) viewedPages.value = next;
|
|
27
|
+
}
|
|
28
|
+
function resetViewedPages() {
|
|
29
|
+
if (viewedPages.value.size === 0) return;
|
|
30
|
+
viewedPages.value = /* @__PURE__ */ new Set();
|
|
31
|
+
}
|
|
8
32
|
let observer = null;
|
|
9
33
|
let scrollRoot = null;
|
|
10
34
|
let scrollListener = null;
|
|
@@ -88,12 +112,19 @@ export function createPageVirtualization() {
|
|
|
88
112
|
if (bestPage !== currentPage.value) {
|
|
89
113
|
currentPage.value = bestPage;
|
|
90
114
|
}
|
|
115
|
+
const viewportBottom = rootTop + scrollRoot.clientHeight;
|
|
116
|
+
const reached = [];
|
|
117
|
+
for (const [pageNum, el] of pageToElement) {
|
|
118
|
+
if (el.offsetTop < viewportBottom) reached.push(pageNum);
|
|
119
|
+
}
|
|
120
|
+
markPagesViewed(reached);
|
|
91
121
|
}
|
|
92
122
|
function scrollToPage(pageNumber) {
|
|
93
123
|
const element = pageToElement.get(pageNumber);
|
|
94
124
|
if (element && scrollRoot) {
|
|
95
125
|
element.scrollIntoView({ block: "start", behavior: "instant" });
|
|
96
126
|
currentPage.value = pageNumber;
|
|
127
|
+
markPageViewed(pageNumber);
|
|
97
128
|
}
|
|
98
129
|
}
|
|
99
130
|
function getPageElement(pageNumber) {
|
|
@@ -103,8 +134,10 @@ export function createPageVirtualization() {
|
|
|
103
134
|
if (!scrollRoot) return;
|
|
104
135
|
const rootRect = scrollRoot.getBoundingClientRect();
|
|
105
136
|
const rootMid = rootRect.top + rootRect.height / 2;
|
|
137
|
+
const rootBottom = rootRect.top + rootRect.height;
|
|
106
138
|
let bestPage = currentPage.value;
|
|
107
139
|
let bestDist = Infinity;
|
|
140
|
+
const reached = [];
|
|
108
141
|
for (const [pageNum, el] of pageToElement) {
|
|
109
142
|
const rect = el.getBoundingClientRect();
|
|
110
143
|
const dist = rootMid < rect.top ? rect.top - rootMid : rootMid > rect.bottom ? rootMid - rect.bottom : 0;
|
|
@@ -112,7 +145,9 @@ export function createPageVirtualization() {
|
|
|
112
145
|
bestDist = dist;
|
|
113
146
|
bestPage = pageNum;
|
|
114
147
|
}
|
|
148
|
+
if (rect.top < rootBottom) reached.push(pageNum);
|
|
115
149
|
}
|
|
150
|
+
markPagesViewed(reached);
|
|
116
151
|
if (bestPage !== currentPage.value) {
|
|
117
152
|
currentPage.value = bestPage;
|
|
118
153
|
}
|
|
@@ -124,10 +159,15 @@ export function createPageVirtualization() {
|
|
|
124
159
|
const vy = view[1] ?? 0;
|
|
125
160
|
const vw = view[2] ?? 0;
|
|
126
161
|
const vh = view[3] ?? 0;
|
|
162
|
+
const w = vw - vx;
|
|
163
|
+
const h = vh - vy;
|
|
164
|
+
const rotation = (proxy.rotate % 360 + 360) % 360;
|
|
165
|
+
const swap = rotation % 180 !== 0;
|
|
127
166
|
return {
|
|
128
167
|
pageNumber,
|
|
129
|
-
width:
|
|
130
|
-
height:
|
|
168
|
+
width: swap ? h : w,
|
|
169
|
+
height: swap ? w : h,
|
|
170
|
+
rotation,
|
|
131
171
|
viewOffsetX: vx,
|
|
132
172
|
viewOffsetY: vy
|
|
133
173
|
};
|
|
@@ -146,6 +186,7 @@ export function createPageVirtualization() {
|
|
|
146
186
|
pageNumber: i,
|
|
147
187
|
width: firstMeta.width,
|
|
148
188
|
height: firstMeta.height,
|
|
189
|
+
rotation: firstMeta.rotation,
|
|
149
190
|
viewOffsetX: firstMeta.viewOffsetX,
|
|
150
191
|
viewOffsetY: firstMeta.viewOffsetY
|
|
151
192
|
});
|
|
@@ -215,6 +256,7 @@ export function createPageVirtualization() {
|
|
|
215
256
|
elementToPage.clear();
|
|
216
257
|
pageToElement.clear();
|
|
217
258
|
visiblePages.value = /* @__PURE__ */ new Set();
|
|
259
|
+
viewedPages.value = /* @__PURE__ */ new Set();
|
|
218
260
|
pageMetas.value = [];
|
|
219
261
|
scrollRoot = null;
|
|
220
262
|
}
|
|
@@ -222,6 +264,10 @@ export function createPageVirtualization() {
|
|
|
222
264
|
pageMetas,
|
|
223
265
|
currentPage,
|
|
224
266
|
renderedPages,
|
|
267
|
+
viewedPages,
|
|
268
|
+
allPagesRead,
|
|
269
|
+
markPageViewed,
|
|
270
|
+
resetViewedPages,
|
|
225
271
|
isPageRendered,
|
|
226
272
|
observePage,
|
|
227
273
|
unobservePage,
|