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
package/README.md CHANGED
@@ -20,6 +20,7 @@ A Nuxt module for viewing, annotating, and exporting PDFs. Built on pdfjs-dist,
20
20
  - ↩️ Undo/redo history
21
21
  - 🗂️ Multi-tab document support (`KViewerTabs`)
22
22
  - 🎨 Customizable header and footer slots
23
+ - 👁️ Read tracking — `allPagesRead()` + `all-pages-read` event to gate "must read all pages" signing
23
24
 
24
25
  ## Quick Setup
25
26
 
@@ -112,6 +113,9 @@ const isEditing = viewerRef.value?.formEditMode.value
112
113
  | `formEditMode` | `Ref<boolean>` |
113
114
  | `setFormEditMode(enabled)` | `void` |
114
115
  | `toggleFormEditMode()` | `boolean` (the new state) |
116
+ | `allPagesRead()` | `boolean` |
117
+ | `getViewedPages()` | `number[]` |
118
+ | `resetViewedPages()` | `void` |
115
119
 
116
120
  `exportPdf` options default to `{ flatten: false, download: false, preserveOriginalAnnotations: false }`.
117
121
 
@@ -138,6 +142,30 @@ Or imperatively from the ref API (`setFormEditMode`, `toggleFormEditMode`, react
138
142
 
139
143
  When native PDF annotations are auto-imported into Konva, exporting with `preserveOriginalAnnotations: true` may duplicate annotations. Prefer `preserveOriginalAnnotations: false` in that workflow.
140
144
 
145
+ ### Read tracking
146
+
147
+ For "must read all pages before signing" flows, `KViewer` tracks which pages have entered the viewport. Gate your sign action on the `all-pages-read` event (push) or the `allPagesRead()` ref method (pull):
148
+
149
+ ```vue
150
+ <template>
151
+ <KViewer
152
+ :source="pdfUrl"
153
+ @all-pages-read="canSign = true"
154
+ @update:viewed-pages="(pages) => (readCount = pages.length)"
155
+ />
156
+ <button :disabled="!canSign">Sign</button>
157
+ </template>
158
+
159
+ <script setup lang="ts">
160
+ const canSign = ref(false)
161
+ const readCount = ref(0)
162
+ </script>
163
+ ```
164
+
165
+ A page counts as read once its top edge scrolls into view (computed from geometry, so a fast scroll to the bottom still marks every page it passed). The viewer deliberately does not enforce that the user dwelled on each page — skipping is the signer's choice. The host app owns the policy (e.g. an extra "I have read all pages" checkbox); the viewer only reports the status. Call `resetViewedPages()` to restart tracking; loading a new `source` resets it automatically.
166
+
167
+ The same surface is available over the [embed bridge](src/runtime/embed): `client.allPagesRead()`, `client.getViewedPages()`, `client.resetViewedPages()`, and the `all-pages-read` / `viewedPages-changed` events via `client.on(...)`.
168
+
141
169
  ## Development
142
170
 
143
171
  <details>
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.1.2",
4
+ "version": "0.2.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname, join } from 'node:path';
3
- import { defineNuxtModule, createResolver, hasNuxtModule, installModule, addPlugin, addComponentsDir, addImports } from '@nuxt/kit';
3
+ import { defineNuxtModule, createResolver, extendViteConfig, addPlugin, addComponentsDir, addImports } from '@nuxt/kit';
4
4
  export { KVIEWER_EMBED_PROTOCOL_VERSION, KViewerEmbedClient, useKViewerEmbedBridge } from '../dist/runtime/public-types.js';
5
5
 
6
6
  const module$1 = defineNuxtModule({
@@ -13,11 +13,32 @@ const module$1 = defineNuxtModule({
13
13
  prefix: "K",
14
14
  minRenderPixelRatio: 2
15
15
  },
16
+ // Declare @nuxt/ui as a hard dependency so Nuxt resolves it as a
17
+ // top-level module before our setup() runs. We can NOT just call
18
+ // installModule('@nuxt/ui') from setup(): Nuxt UI declares its own
19
+ // transitive deps (@nuxt/icon, @nuxtjs/color-mode, @nuxtjs/mdc, …)
20
+ // via the same `moduleDependencies` hook, and that hook only fires
21
+ // for modules resolved at the top level — not for ones installed
22
+ // from inside another module's setup. Going through this hook gives
23
+ // us the same chain Nuxt UI gets when the consumer lists it in their
24
+ // own `modules` array, while still freeing them from having to. The
25
+ // consumer's own '@nuxt/ui' entry (and any options under nuxt.ui)
26
+ // remains authoritative because Nuxt deduplicates modules by name.
27
+ moduleDependencies: {
28
+ "@nuxt/ui": {}
29
+ },
16
30
  async setup(options, nuxt) {
17
31
  const { resolve } = createResolver(import.meta.url);
18
- if (!hasNuxtModule("@nuxt/ui")) {
19
- await installModule("@nuxt/ui");
20
- }
32
+ extendViteConfig((config) => {
33
+ config.optimizeDeps ||= {};
34
+ config.optimizeDeps.include ||= [];
35
+ config.optimizeDeps.include.push(
36
+ "konva",
37
+ "pdf-lib",
38
+ "pdfjs-dist/legacy/build/pdf.mjs",
39
+ "pdfjs-dist/web/pdf_viewer.mjs"
40
+ );
41
+ }, { client: true, server: false });
21
42
  nuxt.options.runtimeConfig.public.kviewer = {
22
43
  ...nuxt.options.runtimeConfig.public.kviewer,
23
44
  minRenderPixelRatio: options.minRenderPixelRatio ?? 2
@@ -37,7 +37,7 @@ export declare const defaultOptions: {
37
37
  };
38
38
  setting: {
39
39
  COLOR: string | undefined;
40
- FONT_SIZE: number | undefined;
40
+ FONT_SIZE: number;
41
41
  HIGHLIGHT_COLOR: string | undefined;
42
42
  STRIKEOUT_COLOR: string | undefined;
43
43
  UNDERLINE_COLOR: string | undefined;
@@ -16,7 +16,7 @@ const colors = [
16
16
  "#7828a4",
17
17
  "#ff00ff"
18
18
  ];
19
- const fontSizes = [14, 16, 18, 20, 22, 24];
19
+ const fontSizes = [8, 10, 12, 14, 16, 18, 20, 22, 24];
20
20
  export const defaultOptions = {
21
21
  colors,
22
22
  fontSize: fontSizes,
@@ -67,7 +67,7 @@ export const defaultOptions = {
67
67
  },
68
68
  setting: {
69
69
  COLOR: colors[0],
70
- FONT_SIZE: fontSizes[2],
70
+ FONT_SIZE: 18,
71
71
  HIGHLIGHT_COLOR: colors[3],
72
72
  STRIKEOUT_COLOR: colors[1],
73
73
  UNDERLINE_COLOR: colors[7],
@@ -48,7 +48,7 @@ export declare class Painter {
48
48
  freehandGroupingDelay?: number;
49
49
  setExternalGesturesEnabled?: (enabled: boolean) => void;
50
50
  });
51
- private editFreeText;
51
+ editFreeText(id: string): Promise<void>;
52
52
  private bindGlobalEvents;
53
53
  private globalKeyUpHandler;
54
54
  private createKonvaStage;
@@ -1,4 +1,5 @@
1
1
  import { PDFHexString } from 'pdf-lib';
2
+ import { type PageTransform } from '../pdf-import/transform.js';
2
3
  export declare function getRGB(color: string): number[];
3
4
  export declare function rgbToPdfColor(input: string | undefined): [number, number, number];
4
5
  export declare function isElementInDOM(element: HTMLElement): boolean;
@@ -19,7 +20,7 @@ export declare function convertKonvaRectToPdfRect(konvaRect: {
19
20
  y: number;
20
21
  width: number;
21
22
  height: number;
22
- }, pageHeight: number): [number, number, number, number];
23
+ }, pageTransform: PageTransform): [number, number, number, number];
23
24
  export declare function stringToPDFHexString(input: string): PDFHexString;
24
25
  export declare function getTimestampString(date?: Date): string;
25
26
  export declare function normalizeColor(input: string): string;
@@ -1,5 +1,6 @@
1
1
  import Konva from "konva";
2
2
  import { PDFHexString } from "pdf-lib";
3
+ import { convertCanvasRectToPdf } from "../pdf-import/transform.js";
3
4
  export function getRGB(color) {
4
5
  if (color.startsWith("#")) {
5
6
  const colorRGB = Number.parseInt(color.slice(1), 16);
@@ -149,13 +150,8 @@ export function getPDFDateTimestamp(dateString) {
149
150
  const date = new Date(Date.UTC(year, month, day, hour, minute, second));
150
151
  return date.getTime() - tzOffset * 60 * 1e3;
151
152
  }
152
- export function convertKonvaRectToPdfRect(konvaRect, pageHeight) {
153
- const { x, y, width, height } = konvaRect;
154
- const pdfX1 = x;
155
- const pdfY1 = pageHeight - y - height;
156
- const pdfX2 = pdfX1 + width;
157
- const pdfY2 = pdfY1 + height;
158
- return [pdfX1, pdfY1, pdfX2, pdfY2];
153
+ export function convertKonvaRectToPdfRect(konvaRect, pageTransform) {
154
+ return convertCanvasRectToPdf(pageTransform, konvaRect);
159
155
  }
160
156
  export function stringToPDFHexString(input) {
161
157
  const bom = [254, 255];
@@ -1,5 +1,6 @@
1
- import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef, PDFString } from "pdf-lib";
1
+ import { degrees, PDFArray, PDFDict, PDFDocument, PDFName, PDFRef, PDFString } from "pdf-lib";
2
2
  import { PdfjsAnnotationType } from "../engine/types.js";
3
+ import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { writeFormFieldsToPdf } from "./export-form-fields.js";
4
5
  import { CircleParser } from "./parse_circle.js";
5
6
  import { FreeTextParser } from "./parse_freetext.js";
@@ -144,41 +145,94 @@ async function embedDataUrlImage(pdfDoc, dataUrl) {
144
145
  }
145
146
  return pdfDoc.embedPng(base64);
146
147
  }
148
+ function loadImagesForNode(node) {
149
+ if (typeof window === "undefined") return [];
150
+ const findFn = node.find;
151
+ if (typeof findFn !== "function") return [];
152
+ const images = findFn.call(node, "Image");
153
+ const tasks = [];
154
+ for (const img of images) {
155
+ if (img.image()) continue;
156
+ const src = img.getAttr("base64");
157
+ if (typeof src !== "string" || src.length === 0) continue;
158
+ tasks.push(new Promise((resolve) => {
159
+ const el = new window.Image();
160
+ el.crossOrigin = "anonymous";
161
+ el.onload = () => {
162
+ img.image(el);
163
+ resolve();
164
+ };
165
+ el.onerror = () => {
166
+ resolve();
167
+ };
168
+ el.src = src;
169
+ }));
170
+ }
171
+ return tasks;
172
+ }
147
173
  async function flattenPageAnnotations(pdfDoc, page, annotations) {
148
174
  if (annotations.length === 0) return;
149
175
  if (typeof window === "undefined" || typeof document === "undefined") {
150
176
  throw new TypeError("Flatten export requires a browser environment");
151
177
  }
152
178
  const Konva = (await import("konva")).default;
153
- const width = Math.floor(page.getWidth());
154
- const height = Math.floor(page.getHeight());
179
+ const pageTransform = pageTransformFromPdfLibPage(page);
180
+ const stageWidth = Math.max(1, Math.floor(pageTransform.width));
181
+ const stageHeight = Math.max(1, Math.floor(pageTransform.height));
155
182
  const stage = new Konva.Stage({
156
183
  container: document.createElement("div"),
157
- width,
158
- height,
184
+ width: stageWidth,
185
+ height: stageHeight,
159
186
  listening: false
160
187
  });
161
188
  const layer = new Konva.Layer({ listening: false });
162
189
  stage.add(layer);
190
+ const imageLoaders = [];
163
191
  for (const annotation of annotations) {
164
192
  try {
165
193
  const node = Konva.Node.create(annotation.konvaString);
166
194
  if (node instanceof Konva.Node) {
167
195
  layer.add(node);
196
+ imageLoaders.push(...loadImagesForNode(node));
168
197
  }
169
198
  } catch {
170
199
  console.warn(`Unable to flatten annotation ${annotation.id}; skipping`);
171
200
  }
172
201
  }
202
+ await Promise.all(imageLoaders);
173
203
  layer.draw();
174
204
  const dataUrl = stage.toDataURL({ pixelRatio: 2 });
175
205
  stage.destroy();
176
206
  const image = await embedDataUrlImage(pdfDoc, dataUrl);
207
+ const W_u = page.getWidth();
208
+ const H_u = page.getHeight();
209
+ const dispW = pageTransform.width;
210
+ const dispH = pageTransform.height;
211
+ let drawX = 0;
212
+ let drawY = 0;
213
+ switch (pageTransform.rotation) {
214
+ case 90:
215
+ drawX = W_u;
216
+ drawY = 0;
217
+ break;
218
+ case 180:
219
+ drawX = W_u;
220
+ drawY = H_u;
221
+ break;
222
+ case 270:
223
+ drawX = 0;
224
+ drawY = H_u;
225
+ break;
226
+ default:
227
+ drawX = 0;
228
+ drawY = 0;
229
+ }
177
230
  page.drawImage(image, {
178
- x: 0,
179
- y: 0,
180
- width: page.getWidth(),
181
- height: page.getHeight()
231
+ x: drawX,
232
+ y: drawY,
233
+ width: dispW,
234
+ height: dispH,
235
+ rotate: degrees(pageTransform.rotation)
182
236
  });
183
237
  }
184
238
  function groupByPage(annotations) {
@@ -1,5 +1,6 @@
1
1
  import { PDFName, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
4
5
  import { computeBoundsFromKonvaString } from "./parse_utils.js";
5
6
  const UNKNOWN_USER = "Unknown user";
@@ -7,9 +8,9 @@ export class CircleParser extends AnnotationParser {
7
8
  async parse() {
8
9
  const { annotation, page, pdfDoc } = this;
9
10
  const context = pdfDoc.context;
10
- const pageHeight = page.getHeight();
11
+ const pageTransform = pageTransformFromPdfLibPage(page);
11
12
  const bounds = computeBoundsFromKonvaString(annotation.konvaString);
12
- const rect = convertKonvaRectToPdfRect(bounds ?? annotation.konvaClientRect, pageHeight);
13
+ const rect = convertKonvaRectToPdfRect(bounds ?? annotation.konvaClientRect, pageTransform);
13
14
  const mainAnn = context.obj({
14
15
  Type: PDFName.of("Annot"),
15
16
  Subtype: PDFName.of("Circle"),
@@ -1,6 +1,8 @@
1
1
  import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { convertCanvasRectToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
5
+ import { formAppearanceMatrix } from "./parse_utils.js";
4
6
  const UNKNOWN_USER = "Unknown user";
5
7
  function formatPdfNumber(value) {
6
8
  if (!Number.isFinite(value)) return "0";
@@ -18,7 +20,7 @@ export class FreeTextParser extends AnnotationParser {
18
20
  async parse() {
19
21
  const { annotation, page, pdfDoc } = this;
20
22
  const context = pdfDoc.context;
21
- const pageHeight = page.getHeight();
23
+ const pageTransform = pageTransformFromPdfLibPage(page);
22
24
  let konvaGroup;
23
25
  try {
24
26
  konvaGroup = JSON.parse(annotation.konvaString);
@@ -47,18 +49,20 @@ export class FreeTextParser extends AnnotationParser {
47
49
  const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0);
48
50
  const textWidthFromNode = textNode?.width ? textNode.width * scaleX : longestLine * estimatedCharWidth;
49
51
  const textWidth = Math.max(1, textWidthFromNode);
50
- const pdfX1 = nodeX;
51
- const pdfY1 = pageHeight - nodeY - textHeight;
52
- const pdfX2 = pdfX1 + textWidth;
53
- const pdfY2 = pageHeight - nodeY;
52
+ const [pdfX1, pdfY1, pdfX2, pdfY2] = convertCanvasRectToPdf(pageTransform, {
53
+ x: nodeX,
54
+ y: nodeY,
55
+ width: textWidth,
56
+ height: textHeight
57
+ });
54
58
  const rect = [
55
59
  PDFNumber.of(pdfX1),
56
60
  PDFNumber.of(pdfY1),
57
61
  PDFNumber.of(pdfX2),
58
62
  PDFNumber.of(pdfY2)
59
63
  ];
60
- const appearanceWidth = Math.max(1, pdfX2 - pdfX1);
61
- const appearanceHeight = Math.max(1, pdfY2 - pdfY1);
64
+ const appearanceWidth = Math.max(1, textWidth);
65
+ const appearanceHeight = Math.max(1, textHeight);
62
66
  const commands = ["q"];
63
67
  for (let i = 0; i < lines.length; i++) {
64
68
  const line = lines[i] ?? "";
@@ -89,11 +93,17 @@ export class FreeTextParser extends AnnotationParser {
89
93
  Encoding: PDFName.of("WinAnsiEncoding")
90
94
  });
91
95
  const fontRef = context.register(fontDict);
96
+ const appearanceMatrix = formAppearanceMatrix(
97
+ pageTransform.rotation,
98
+ appearanceWidth,
99
+ appearanceHeight
100
+ );
92
101
  const appearanceStream = PDFRawStream.of(
93
102
  context.obj({
94
103
  Type: PDFName.of("XObject"),
95
104
  Subtype: PDFName.of("Form"),
96
105
  BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
106
+ Matrix: context.obj(appearanceMatrix.map((n) => PDFNumber.of(n))),
97
107
  Resources: context.obj({
98
108
  Font: context.obj({ Helvetica: fontRef })
99
109
  })
@@ -125,7 +135,7 @@ export class FreeTextParser extends AnnotationParser {
125
135
  const replyAnn = context.obj({
126
136
  Type: PDFName.of("Annot"),
127
137
  Subtype: PDFName.of("Text"),
128
- Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight),
138
+ Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
129
139
  Contents: stringToPDFHexString(comment.content),
130
140
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
131
141
  M: PDFString.of(comment.date || ""),
@@ -1,5 +1,10 @@
1
1
  import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import {
4
+ convertCanvasPointToPdf,
5
+ convertCanvasRectToPdf,
6
+ pageTransformFromPdfLibPage
7
+ } from "../pdf-import/transform.js";
3
8
  import { AnnotationParser } from "./parse.js";
4
9
  const UNKNOWN_USER = "Unknown user";
5
10
  function formatPdfNumber(value) {
@@ -21,18 +26,14 @@ function parseHighlightRects(konvaString) {
21
26
  return { x, y, width, height };
22
27
  }).filter((rect) => rect.width > 0 && rect.height > 0);
23
28
  }
24
- function buildQuadPoints(rects, pageHeight) {
29
+ function buildQuadPoints(rects, t) {
25
30
  const quadPoints = [];
26
- for (const rect of rects) {
27
- const x = rect.x;
28
- const y = rect.y;
29
- const width = rect.width;
30
- const height = rect.height;
31
- const x1 = x;
32
- const y1 = pageHeight - y;
33
- const x2 = x + width;
34
- const y2 = pageHeight - (y + height);
35
- quadPoints.push(x1, y1, x2, y1, x1, y2, x2, y2);
31
+ for (const r of rects) {
32
+ const tl = convertCanvasPointToPdf(t, r.x, r.y);
33
+ const tr = convertCanvasPointToPdf(t, r.x + r.width, r.y);
34
+ const bl = convertCanvasPointToPdf(t, r.x, r.y + r.height);
35
+ const br = convertCanvasPointToPdf(t, r.x + r.width, r.y + r.height);
36
+ quadPoints.push(tl.x, tl.y, tr.x, tr.y, bl.x, bl.y, br.x, br.y);
36
37
  }
37
38
  return quadPoints;
38
39
  }
@@ -40,7 +41,7 @@ export class HighlightParser extends AnnotationParser {
40
41
  async parse() {
41
42
  const { annotation, page, pdfDoc } = this;
42
43
  const context = pdfDoc.context;
43
- const pageHeight = page.getHeight();
44
+ const pageTransform = pageTransformFromPdfLibPage(page);
44
45
  let highlightRects = [];
45
46
  try {
46
47
  highlightRects = parseHighlightRects(annotation.konvaString);
@@ -55,21 +56,18 @@ export class HighlightParser extends AnnotationParser {
55
56
  height: annotation.konvaClientRect.height
56
57
  }];
57
58
  }
58
- const quadPoints = buildQuadPoints(highlightRects, pageHeight);
59
+ const quadPoints = buildQuadPoints(highlightRects, pageTransform);
59
60
  const color = rgbToPdfColor(annotation.color || void 0);
61
+ const userRects = highlightRects.map((hr) => convertCanvasRectToPdf(pageTransform, hr));
60
62
  let minX = Infinity;
61
63
  let minY = Infinity;
62
64
  let maxX = -Infinity;
63
65
  let maxY = -Infinity;
64
- for (const hr of highlightRects) {
65
- const pdfLeft = hr.x;
66
- const pdfBottom = pageHeight - hr.y - hr.height;
67
- const pdfRight = hr.x + hr.width;
68
- const pdfTop = pageHeight - hr.y;
69
- if (pdfLeft < minX) minX = pdfLeft;
70
- if (pdfBottom < minY) minY = pdfBottom;
71
- if (pdfRight > maxX) maxX = pdfRight;
72
- if (pdfTop > maxY) maxY = pdfTop;
66
+ for (const [ux1, uy1, ux2, uy2] of userRects) {
67
+ if (ux1 < minX) minX = ux1;
68
+ if (uy1 < minY) minY = uy1;
69
+ if (ux2 > maxX) maxX = ux2;
70
+ if (uy2 > maxY) maxY = uy2;
73
71
  }
74
72
  const x1 = minX;
75
73
  const y1 = minY;
@@ -84,13 +82,13 @@ export class HighlightParser extends AnnotationParser {
84
82
  "/GS1 gs",
85
83
  `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`
86
84
  ];
87
- for (const highlightRect of highlightRects) {
88
- const pageX = highlightRect.x;
89
- const pageY = pageHeight - highlightRect.y - highlightRect.height;
90
- const localX = pageX - x1;
91
- const localY = pageY - y1;
85
+ for (const [ux1, uy1, ux2, uy2] of userRects) {
86
+ const localX = ux1 - x1;
87
+ const localY = uy1 - y1;
88
+ const w = ux2 - ux1;
89
+ const h = uy2 - uy1;
92
90
  commands.push(
93
- `${formatPdfNumber(localX)} ${formatPdfNumber(localY)} ${formatPdfNumber(highlightRect.width)} ${formatPdfNumber(highlightRect.height)} re`,
91
+ `${formatPdfNumber(localX)} ${formatPdfNumber(localY)} ${formatPdfNumber(w)} ${formatPdfNumber(h)} re`,
94
92
  "f"
95
93
  );
96
94
  }
@@ -133,7 +131,7 @@ export class HighlightParser extends AnnotationParser {
133
131
  const replyAnn = context.obj({
134
132
  Type: PDFName.of("Annot"),
135
133
  Subtype: PDFName.of("Text"),
136
- Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight),
134
+ Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
137
135
  Contents: stringToPDFHexString(comment.content),
138
136
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
139
137
  M: PDFString.of(comment.date || ""),
@@ -1,5 +1,6 @@
1
1
  import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
4
5
  const UNKNOWN_USER = "Unknown user";
5
6
  function formatPdfNumber(value) {
@@ -10,7 +11,7 @@ export class InkParser extends AnnotationParser {
10
11
  async parse() {
11
12
  const { annotation, page, pdfDoc } = this;
12
13
  const context = pdfDoc.context;
13
- const pageHeight = page.getHeight();
14
+ const pageTransform = pageTransformFromPdfLibPage(page);
14
15
  let konvaGroup;
15
16
  try {
16
17
  konvaGroup = JSON.parse(annotation.konvaString);
@@ -31,7 +32,8 @@ export class InkParser extends AnnotationParser {
31
32
  for (let i = 0; i < points.length; i += 2) {
32
33
  const x = groupX + points[i] * scaleX;
33
34
  const y = groupY + points[i + 1] * scaleY;
34
- transformedPoints.push(x, pageHeight - y);
35
+ const u = convertCanvasPointToPdf(pageTransform, x, y);
36
+ transformedPoints.push(u.x, u.y);
35
37
  }
36
38
  transformedPaths.push(transformedPoints);
37
39
  }
@@ -136,7 +138,7 @@ export class InkParser extends AnnotationParser {
136
138
  const replyAnn = context.obj({
137
139
  Type: PDFName.of("Annot"),
138
140
  Subtype: PDFName.of("Text"),
139
- Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight),
141
+ Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
140
142
  Contents: stringToPDFHexString(comment.content),
141
143
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
142
144
  M: PDFString.of(comment.date || ""),
@@ -1,5 +1,6 @@
1
1
  import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
4
5
  const UNKNOWN_USER = "Unknown user";
5
6
  function formatPdfNumber(value) {
@@ -10,7 +11,7 @@ export class LineParser extends AnnotationParser {
10
11
  async parse() {
11
12
  const { annotation, page, pdfDoc } = this;
12
13
  const context = pdfDoc.context;
13
- const pageHeight = page.getHeight();
14
+ const pageTransform = pageTransformFromPdfLibPage(page);
14
15
  const konvaGroup = JSON.parse(annotation.konvaString);
15
16
  const lines = (konvaGroup.children ?? []).filter((item) => item.className === "Arrow");
16
17
  const groupX = konvaGroup.attrs?.x || 0;
@@ -25,7 +26,8 @@ export class LineParser extends AnnotationParser {
25
26
  for (let i = 0; i < points.length; i += 2) {
26
27
  const x = groupX + (points[i] ?? 0) * scaleX;
27
28
  const y = groupY + (points[i + 1] ?? 0) * scaleY;
28
- transformedPoints.push(x, pageHeight - y);
29
+ const u = convertCanvasPointToPdf(pageTransform, x, y);
30
+ transformedPoints.push(u.x, u.y);
29
31
  }
30
32
  transformedPaths.push(transformedPoints);
31
33
  }
@@ -174,7 +176,7 @@ export class LineParser extends AnnotationParser {
174
176
  const replyAnn = context.obj({
175
177
  Type: PDFName.of("Annot"),
176
178
  Subtype: PDFName.of("Text"),
177
- Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight),
179
+ Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
178
180
  Contents: stringToPDFHexString(comment.content),
179
181
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
180
182
  M: PDFString.of(comment.date || ""),
@@ -1,5 +1,6 @@
1
1
  import { PDFName, PDFNumber, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
4
5
  import { computeBoundsFromKonvaString } from "./parse_utils.js";
5
6
  const UNKNOWN_USER = "Unknown user";
@@ -29,7 +30,7 @@ export class PolylineParser extends AnnotationParser {
29
30
  async parse() {
30
31
  const { annotation, page, pdfDoc } = this;
31
32
  const context = pdfDoc.context;
32
- const pageHeight = page.getHeight();
33
+ const pageTransform = pageTransformFromPdfLibPage(page);
33
34
  const konvaGroup = JSON.parse(annotation.konvaString);
34
35
  const lines = (konvaGroup.children ?? []).filter((item) => item.className === "Path");
35
36
  const groupX = konvaGroup.attrs?.x || 0;
@@ -43,7 +44,8 @@ export class PolylineParser extends AnnotationParser {
43
44
  for (let i = 0; i < points.length; i += 2) {
44
45
  const x = groupX + points[i] * scaleX;
45
46
  const y = groupY + points[i + 1] * scaleY;
46
- transformedPoints.push(x, pageHeight - y);
47
+ const u = convertCanvasPointToPdf(pageTransform, x, y);
48
+ transformedPoints.push(u.x, u.y);
47
49
  }
48
50
  return context.obj(transformedPoints);
49
51
  })
@@ -62,7 +64,7 @@ export class PolylineParser extends AnnotationParser {
62
64
  Subtype: PDFName.of("Ink"),
63
65
  // Compute Rect from konvaString so it reflects the current position
64
66
  // even when konvaClientRect is stale after a move/resize.
65
- Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageHeight),
67
+ Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageTransform),
66
68
  InkList: inkList,
67
69
  C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
68
70
  T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
@@ -79,7 +81,7 @@ export class PolylineParser extends AnnotationParser {
79
81
  const replyAnn = context.obj({
80
82
  Type: PDFName.of("Annot"),
81
83
  Subtype: PDFName.of("Text"),
82
- Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageHeight),
84
+ Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageTransform),
83
85
  Contents: stringToPDFHexString(comment.content),
84
86
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
85
87
  M: PDFString.of(comment.date || ""),
@@ -1,5 +1,6 @@
1
1
  import { PDFName, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
+ import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
3
4
  import { AnnotationParser } from "./parse.js";
4
5
  import { computeBoundsFromKonvaString } from "./parse_utils.js";
5
6
  const UNKNOWN_USER = "Unknown user";
@@ -7,9 +8,9 @@ export class SquareParser extends AnnotationParser {
7
8
  async parse() {
8
9
  const { annotation, page, pdfDoc } = this;
9
10
  const context = pdfDoc.context;
10
- const pageHeight = page.getHeight();
11
+ const pageTransform = pageTransformFromPdfLibPage(page);
11
12
  const bounds = computeBoundsFromKonvaString(annotation.konvaString);
12
- const rect = convertKonvaRectToPdfRect(bounds ?? annotation.konvaClientRect, pageHeight);
13
+ const rect = convertKonvaRectToPdfRect(bounds ?? annotation.konvaClientRect, pageTransform);
13
14
  const mainAnn = context.obj({
14
15
  Type: PDFName.of("Annot"),
15
16
  Subtype: PDFName.of("Square"),