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.
Files changed (58) hide show
  1. package/README.md +28 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +25 -4
  4. package/dist/runtime/annotation/engine/config.d.ts +1 -1
  5. package/dist/runtime/annotation/engine/config.js +2 -2
  6. package/dist/runtime/annotation/engine/painter.d.ts +1 -1
  7. package/dist/runtime/annotation/engine/utils.d.ts +2 -1
  8. package/dist/runtime/annotation/engine/utils.js +3 -7
  9. package/dist/runtime/annotation/pdf-export/export.js +63 -9
  10. package/dist/runtime/annotation/pdf-export/parse_circle.js +3 -2
  11. package/dist/runtime/annotation/pdf-export/parse_freetext.js +18 -8
  12. package/dist/runtime/annotation/pdf-export/parse_highlight.js +27 -29
  13. package/dist/runtime/annotation/pdf-export/parse_ink.js +5 -3
  14. package/dist/runtime/annotation/pdf-export/parse_line.js +5 -3
  15. package/dist/runtime/annotation/pdf-export/parse_polyline.js +6 -4
  16. package/dist/runtime/annotation/pdf-export/parse_square.js +3 -2
  17. package/dist/runtime/annotation/pdf-export/parse_stamp.js +16 -9
  18. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +11 -10
  19. package/dist/runtime/annotation/pdf-export/parse_text.js +3 -1
  20. package/dist/runtime/annotation/pdf-export/parse_underline.js +11 -10
  21. package/dist/runtime/annotation/pdf-export/parse_utils.d.ts +11 -0
  22. package/dist/runtime/annotation/pdf-export/parse_utils.js +12 -0
  23. package/dist/runtime/annotation/pdf-import/decode.d.ts +2 -1
  24. package/dist/runtime/annotation/pdf-import/decode.js +6 -5
  25. package/dist/runtime/annotation/pdf-import/decode_circle.js +1 -1
  26. package/dist/runtime/annotation/pdf-import/decode_freetext.js +1 -1
  27. package/dist/runtime/annotation/pdf-import/decode_highlight.js +1 -1
  28. package/dist/runtime/annotation/pdf-import/decode_ink.js +1 -1
  29. package/dist/runtime/annotation/pdf-import/decode_line.js +1 -1
  30. package/dist/runtime/annotation/pdf-import/decode_square.js +1 -1
  31. package/dist/runtime/annotation/pdf-import/decode_stamp.js +1 -1
  32. package/dist/runtime/annotation/pdf-import/decode_text.js +1 -1
  33. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +3 -2
  34. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +7 -15
  35. package/dist/runtime/annotation/pdf-import/transform.d.ts +85 -0
  36. package/dist/runtime/annotation/pdf-import/transform.js +81 -0
  37. package/dist/runtime/annotation/pdf-import/types.d.ts +5 -1
  38. package/dist/runtime/annotation/pdf-import/utils.d.ts +6 -5
  39. package/dist/runtime/annotation/pdf-import/utils.js +53 -38
  40. package/dist/runtime/components/AnnotationToolbar.vue +16 -0
  41. package/dist/runtime/components/FormFieldLayer.d.vue.ts +4 -7
  42. package/dist/runtime/components/FormFieldLayer.vue +5 -10
  43. package/dist/runtime/components/FormFieldLayer.vue.d.ts +4 -7
  44. package/dist/runtime/components/PdfPage.vue +1 -6
  45. package/dist/runtime/components/Viewer.d.vue.ts +12 -0
  46. package/dist/runtime/components/Viewer.vue +35 -2
  47. package/dist/runtime/components/Viewer.vue.d.ts +12 -0
  48. package/dist/runtime/components/form-fields/FormFieldWrapper.d.vue.ts +5 -6
  49. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +8 -14
  50. package/dist/runtime/components/form-fields/FormFieldWrapper.vue.d.ts +5 -6
  51. package/dist/runtime/composables/usePageVirtualization.d.ts +17 -0
  52. package/dist/runtime/composables/usePageVirtualization.js +48 -2
  53. package/dist/runtime/embed/bridge-client.d.ts +9 -1
  54. package/dist/runtime/embed/bridge-client.js +13 -0
  55. package/dist/runtime/embed/bridge-host.d.ts +3 -0
  56. package/dist/runtime/embed/bridge-host.js +30 -8
  57. package/dist/runtime/embed/protocol.d.ts +21 -0
  58. 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, pageHeight: number): Rect | null;
21
- export declare function pointFromPdfPoint(input: unknown, pageHeight: number): {
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, pageHeight: number): Rect | null;
30
- export declare function pointsFromInkList(input: unknown, pageHeight: number): number[];
31
- export declare function linePointsFromCoordinates(input: unknown, pageHeight: number): number[] | null;
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, pageHeight) {
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, pageHeight) {
121
+ export function pointFromPdfPoint(input, transform) {
123
122
  const pair = toNumericTuple(input, 2);
124
- if (pair) return { x: pair[0], y: pageHeight - pair[1] };
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 { x: point.x, y: pageHeight - point.y };
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
- export function rectFromQuadPoints(input, pageHeight) {
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
- const p02 = pointFromPdfPoint([numeric[0], numeric[1]], pageHeight);
140
- const p12 = pointFromPdfPoint([numeric[2], numeric[3]], pageHeight);
141
- const p32 = pointFromPdfPoint([numeric[6], numeric[7]], pageHeight);
142
- if (!p02 || !p12 || !p32) return null;
143
- return {
144
- x: p02.x,
145
- y: p02.y,
146
- width: p12.x - p02.x,
147
- height: p12.y - p32.y
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 (!Array.isArray(input) || input.length < 4) return null;
151
- const p0 = pointFromPdfPoint(input[0], pageHeight);
152
- const p1 = pointFromPdfPoint(input[1], pageHeight);
153
- const p3 = pointFromPdfPoint(input[3], pageHeight);
154
- if (!p0 || !p1 || !p3) return null;
155
- return {
156
- x: p0.x,
157
- y: p0.y,
158
- width: p1.x - p0.x,
159
- height: p1.y - p3.y
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, pageHeight) {
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
- points2.push(numeric[index], pageHeight - numeric[index + 1]);
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, pageHeight);
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, pageHeight) {
194
+ export function linePointsFromCoordinates(input, transform) {
181
195
  const tuple = toNumericTuple(input, 4);
182
196
  if (!tuple) return null;
183
- const [x, y, x1, y1] = tuple;
184
- return [x, pageHeight - y, x1, pageHeight - y1];
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
- pageWidth: number;
5
- pageHeight: number;
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.pageWidth + 'px',
10
- height: props.pageHeight + 'px',
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-height="props.pageHeight"
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
- pageWidth: { type: Number, required: true },
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
- pageWidth: number;
5
- pageHeight: number;
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-width="baseWidth"
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
- pageHeight: number;
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.pageHeight"
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
- pageHeight: { type: Number, required: true },
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 vx = props.viewOffsetX ?? 0;
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: `${left}px`,
76
- top: `${top}px`,
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
- pageHeight: number;
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: vw - vx,
130
- height: vh - vy,
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,