kviewer 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +3 -0
  2. package/dist/module.json +2 -2
  3. package/dist/runtime/annotation/engine/painter.d.ts +7 -0
  4. package/dist/runtime/annotation/engine/painter.js +14 -0
  5. package/dist/runtime/annotation/engine/store.d.ts +5 -0
  6. package/dist/runtime/annotation/engine/store.js +17 -0
  7. package/dist/runtime/annotation/engine/tools/free-text.js +3 -1
  8. package/dist/runtime/annotation/engine/types.d.ts +20 -0
  9. package/dist/runtime/annotation/pdf-export/export.d.ts +4 -1
  10. package/dist/runtime/annotation/pdf-export/export.js +101 -4
  11. package/dist/runtime/annotation/pdf-export/parse_freetext.js +106 -18
  12. package/dist/runtime/annotation/pdf-export/parse_highlight.js +22 -6
  13. package/dist/runtime/annotation/pdf-export/parse_ink.js +32 -3
  14. package/dist/runtime/annotation/pdf-export/parse_line.js +133 -13
  15. package/dist/runtime/annotation/pdf-import/decode.d.ts +21 -0
  16. package/dist/runtime/annotation/pdf-import/decode.js +134 -0
  17. package/dist/runtime/annotation/pdf-import/decode_circle.d.ts +3 -0
  18. package/dist/runtime/annotation/pdf-import/decode_circle.js +36 -0
  19. package/dist/runtime/annotation/pdf-import/decode_freetext.d.ts +3 -0
  20. package/dist/runtime/annotation/pdf-import/decode_freetext.js +87 -0
  21. package/dist/runtime/annotation/pdf-import/decode_highlight.d.ts +3 -0
  22. package/dist/runtime/annotation/pdf-import/decode_highlight.js +96 -0
  23. package/dist/runtime/annotation/pdf-import/decode_ink.d.ts +3 -0
  24. package/dist/runtime/annotation/pdf-import/decode_ink.js +48 -0
  25. package/dist/runtime/annotation/pdf-import/decode_line.d.ts +3 -0
  26. package/dist/runtime/annotation/pdf-import/decode_line.js +48 -0
  27. package/dist/runtime/annotation/pdf-import/decode_square.d.ts +3 -0
  28. package/dist/runtime/annotation/pdf-import/decode_square.js +39 -0
  29. package/dist/runtime/annotation/pdf-import/decode_stamp.d.ts +3 -0
  30. package/dist/runtime/annotation/pdf-import/decode_stamp.js +38 -0
  31. package/dist/runtime/annotation/pdf-import/decode_text.d.ts +3 -0
  32. package/dist/runtime/annotation/pdf-import/decode_text.js +33 -0
  33. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +23 -0
  34. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +168 -0
  35. package/dist/runtime/annotation/pdf-import/types.d.ts +57 -0
  36. package/dist/runtime/annotation/pdf-import/types.js +25 -0
  37. package/dist/runtime/annotation/pdf-import/utils.d.ts +48 -0
  38. package/dist/runtime/annotation/pdf-import/utils.js +250 -0
  39. package/dist/runtime/assets/kviewer.css +1 -1
  40. package/dist/runtime/components/AnnotationToolbar.vue +1 -0
  41. package/dist/runtime/components/FloatingPageIndicator.vue +4 -1
  42. package/dist/runtime/components/PdfPage.vue +27 -1
  43. package/dist/runtime/components/Viewer.d.vue.ts +12 -1
  44. package/dist/runtime/components/Viewer.vue +114 -35
  45. package/dist/runtime/components/Viewer.vue.d.ts +12 -1
  46. package/dist/runtime/components/ViewerBar.vue +3 -14
  47. package/dist/runtime/components/ViewerTabs.d.vue.ts +16 -1
  48. package/dist/runtime/components/ViewerTabs.vue +42 -12
  49. package/dist/runtime/components/ViewerTabs.vue.d.ts +16 -1
  50. package/dist/runtime/components/form-fields/FormCheckbox.vue +37 -1
  51. package/dist/runtime/components/tools/ActionTools.vue +3 -0
  52. package/dist/runtime/components/tools/PageInfo.vue +1 -1
  53. package/dist/runtime/components/tools/SearchTool.vue +3 -1
  54. package/dist/runtime/components/tools/ZoomControls.vue +3 -0
  55. package/dist/runtime/composables/shape-detection-utils.d.ts +38 -0
  56. package/dist/runtime/composables/shape-detection-utils.js +50 -0
  57. package/dist/runtime/composables/useFormFields.d.ts +3 -1
  58. package/dist/runtime/composables/useFormFields.js +47 -0
  59. package/dist/runtime/composables/useShapeDetection.d.ts +24 -0
  60. package/dist/runtime/composables/useShapeDetection.js +235 -0
  61. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  62. package/dist/runtime/composables/useViewerState.js +13 -1
  63. package/package.json +28 -5
package/README.md CHANGED
@@ -13,6 +13,7 @@ A Nuxt module for viewing, annotating, and exporting PDFs. Built on pdfjs-dist,
13
13
  - ✏️ 14 annotation tools: Select, Highlight, Strikeout, Underline, Free Text, Signature, Rectangle, Circle, Arrow, Cloud, Freehand, Free Highlight, Stamp, Note
14
14
  - 📤 Export PDF with or without flattened annotations
15
15
  - 💾 Import/export annotation state for draft saving
16
+ - 🔄 Native PDF annotations are auto-imported into editable Konva annotations (core set: Text/FreeText/Highlight/Underline/StrikeOut/Square/Circle/Ink/Line)
16
17
  - 📝 Interactive form fields (text, checkbox, radio, dropdown, signature, button)
17
18
  - 🔍 Full-text search with match highlighting
18
19
  - ⚡ Virtual scrolling for large documents
@@ -94,6 +95,8 @@ viewerRef.value?.setFormFieldValue('email', 'user@example.com')
94
95
 
95
96
  `exportPdf` options default to `{ flatten: false, download: false, preserveOriginalAnnotations: false }`.
96
97
 
98
+ When native PDF annotations are auto-imported into Konva, exporting with `preserveOriginalAnnotations: true` may duplicate annotations. Prefer `preserveOriginalAnnotations: false` in that workflow.
99
+
97
100
  ## Development
98
101
 
99
102
  <details>
package/dist/module.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
- "unbuild": "unknown"
7
+ "unbuild": "3.6.1"
8
8
  }
9
9
  }
@@ -90,6 +90,13 @@ export declare class Painter {
90
90
  updateAnnotationStyle(annotationStore: IAnnotationStore, style: IAnnotationStyle): void;
91
91
  reAddAnnotation(annotation: IAnnotationStore): void;
92
92
  getData(): IAnnotationStore[];
93
+ getExportContext(): {
94
+ annotations: IAnnotationStore[];
95
+ changedAnnotations: IAnnotationStore[];
96
+ originalIds: Set<string>;
97
+ modifiedIds: Set<string>;
98
+ deletedIds: Set<string>;
99
+ };
93
100
  getAnnotation(id: string): IAnnotationStore | undefined;
94
101
  clearAllAnnotations(emit?: boolean): void;
95
102
  replaceAnnotations(annotations: IAnnotationStore[]): void;
@@ -546,6 +546,20 @@ export class Painter {
546
546
  getData() {
547
547
  return this.store.annotations;
548
548
  }
549
+ getExportContext() {
550
+ const originalIds = this.store.originalIds;
551
+ const modifiedIds = this.store.modifiedOriginals;
552
+ const changedAnnotations = this.store.annotations.filter(
553
+ (a) => !originalIds.has(a.id) || modifiedIds.has(a.id)
554
+ );
555
+ return {
556
+ annotations: this.store.annotations,
557
+ changedAnnotations,
558
+ originalIds,
559
+ modifiedIds,
560
+ deletedIds: this.store.deletedOriginals
561
+ };
562
+ }
549
563
  getAnnotation(id) {
550
564
  return this.store.annotation(id);
551
565
  }
@@ -2,10 +2,15 @@ import type { IAnnotationStore } from './types.js';
2
2
  export declare class Store {
3
3
  private annotationStore;
4
4
  private originalAnnotationStore;
5
+ private deletedOriginalIds;
6
+ private modifiedOriginalIds;
5
7
  get annotation(): (id: string) => IAnnotationStore | undefined;
6
8
  get annotations(): IAnnotationStore[];
7
9
  save(store: IAnnotationStore, isOriginal: boolean): IAnnotationStore;
8
10
  update(id: string, updates: Partial<IAnnotationStore>): IAnnotationStore | null;
9
11
  getByPage(pageNumber: number): IAnnotationStore[];
10
12
  delete(id: string): void;
13
+ get originalIds(): Set<string>;
14
+ get deletedOriginals(): Set<string>;
15
+ get modifiedOriginals(): Set<string>;
11
16
  }
@@ -2,6 +2,8 @@ import { formatTimestamp } from "./utils.js";
2
2
  export class Store {
3
3
  annotationStore = /* @__PURE__ */ new Map();
4
4
  originalAnnotationStore = /* @__PURE__ */ new Map();
5
+ deletedOriginalIds = /* @__PURE__ */ new Set();
6
+ modifiedOriginalIds = /* @__PURE__ */ new Set();
5
7
  get annotation() {
6
8
  return (id) => this.annotationStore.get(id);
7
9
  }
@@ -25,6 +27,9 @@ export class Store {
25
27
  date: formatTimestamp(Date.now())
26
28
  };
27
29
  this.annotationStore.set(id, updatedAnnotation);
30
+ if (this.originalAnnotationStore.has(id)) {
31
+ this.modifiedOriginalIds.add(id);
32
+ }
28
33
  return updatedAnnotation;
29
34
  }
30
35
  } else {
@@ -40,8 +45,20 @@ export class Store {
40
45
  delete(id) {
41
46
  if (this.annotationStore.has(id)) {
42
47
  this.annotationStore.delete(id);
48
+ if (this.originalAnnotationStore.has(id)) {
49
+ this.deletedOriginalIds.add(id);
50
+ }
43
51
  } else {
44
52
  console.warn(`Annotation with id ${id} not found.`);
45
53
  }
46
54
  }
55
+ get originalIds() {
56
+ return new Set(this.originalAnnotationStore.keys());
57
+ }
58
+ get deletedOriginals() {
59
+ return new Set(this.deletedOriginalIds);
60
+ }
61
+ get modifiedOriginals() {
62
+ return new Set(this.modifiedOriginalIds);
63
+ }
47
64
  }
@@ -74,7 +74,9 @@ export class EditorFreeText extends Editor {
74
74
  const { finalWidth, wrap } = EditorFreeText.measureText(value, fontSize);
75
75
  group.getChildren().forEach((shape) => {
76
76
  if (shape instanceof Konva.Text) {
77
- shape.setAttrs({ text: value, fill: color, fontSize, width: finalWidth, wrap });
77
+ const existingWidth = shape.width();
78
+ const keepWidth = existingWidth > 0 ? Math.max(existingWidth, finalWidth) : finalWidth;
79
+ shape.setAttrs({ text: value, fill: color, fontSize, width: keepWidth, wrap: keepWidth > finalWidth ? "word" : wrap });
78
80
  }
79
81
  });
80
82
  this.setChanged(id, {
@@ -142,6 +142,8 @@ export interface FormFieldDefinition {
142
142
  rect: [number, number, number, number];
143
143
  readOnly: boolean;
144
144
  required: boolean;
145
+ /** Visual shape hint for detected checkboxes (circle renders round, rectangle renders square). */
146
+ shapeType?: DetectedShapeType;
145
147
  defaultValue?: string;
146
148
  maxLen?: number;
147
149
  multiLine?: boolean;
@@ -168,3 +170,21 @@ export interface FormFieldValue {
168
170
  fieldType: FormFieldType;
169
171
  value: string | boolean | string[];
170
172
  }
173
+ export interface DetectedShapeRect {
174
+ x: number;
175
+ y: number;
176
+ w: number;
177
+ h: number;
178
+ }
179
+ export interface DetectedShapeCenter {
180
+ x: number;
181
+ y: number;
182
+ }
183
+ export type DetectedShapeType = 'rectangle' | 'circle';
184
+ export interface DetectedShape {
185
+ id: string;
186
+ rect: DetectedShapeRect;
187
+ center: DetectedShapeCenter;
188
+ size: number;
189
+ shapeType?: DetectedShapeType;
190
+ }
@@ -10,6 +10,9 @@ interface ExportPdfParams {
10
10
  annotations: IAnnotationStore[];
11
11
  options?: ExportPdfOptions;
12
12
  formFieldValues?: FormFieldValue[];
13
+ originalAnnotationIds?: Set<string>;
14
+ modifiedAnnotationIds?: Set<string>;
15
+ deletedAnnotationIds?: Set<string>;
13
16
  }
14
- export declare function exportAnnotationsToPdf({ pdfData, annotations, options, formFieldValues, }: ExportPdfParams): Promise<Uint8Array>;
17
+ export declare function exportAnnotationsToPdf({ pdfData, annotations, options, formFieldValues, originalAnnotationIds, modifiedAnnotationIds, deletedAnnotationIds, }: ExportPdfParams): Promise<Uint8Array>;
15
18
  export {};
@@ -1,4 +1,4 @@
1
- import { PDFDocument, PDFName } from "pdf-lib";
1
+ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef, PDFString } from "pdf-lib";
2
2
  import { PdfjsAnnotationType } from "../engine/types.js";
3
3
  import { writeFormFieldsToPdf } from "./export-form-fields.js";
4
4
  import { CircleParser } from "./parse_circle.js";
@@ -37,6 +37,92 @@ function clearAllAnnotations(pdfDoc) {
37
37
  }
38
38
  }
39
39
  }
40
+ const ALWAYS_PRESERVE_SUBTYPES = /* @__PURE__ */ new Set([
41
+ "Widget",
42
+ "Link",
43
+ "Popup",
44
+ "Sound",
45
+ "Movie",
46
+ "Screen",
47
+ "PrinterMark",
48
+ "TrapNet",
49
+ "Watermark",
50
+ "3D",
51
+ "Redact",
52
+ "FileAttachment",
53
+ "Caret"
54
+ ]);
55
+ function resolveAnnotDict(ref, context) {
56
+ if (ref instanceof PDFRef) {
57
+ const looked = context.lookup(ref);
58
+ return looked instanceof PDFDict ? looked : null;
59
+ }
60
+ if (ref instanceof PDFDict) {
61
+ return ref;
62
+ }
63
+ return null;
64
+ }
65
+ function getAnnotNM(dict) {
66
+ const nm = dict.get(PDFName.of("NM"));
67
+ if (nm instanceof PDFString) return nm.decodeText();
68
+ return null;
69
+ }
70
+ function getAnnotSubtype(dict) {
71
+ const sub = dict.get(PDFName.of("Subtype"));
72
+ if (sub instanceof PDFName) return sub.decodeText();
73
+ return null;
74
+ }
75
+ function getAnnotIRT(dict) {
76
+ return dict.get(PDFName.of("IRT")) ?? null;
77
+ }
78
+ function refMatchesId(ref, ids) {
79
+ if (!(ref instanceof PDFRef)) return false;
80
+ return ids.has(`${ref.objectNumber}R`);
81
+ }
82
+ function removeOwnedAnnotations(pdfDoc, ownedIds, deletedIds) {
83
+ const context = pdfDoc.context;
84
+ const idsToRemove = /* @__PURE__ */ new Set([...ownedIds, ...deletedIds]);
85
+ for (const page of pdfDoc.getPages()) {
86
+ const annotsKey = PDFName.of("Annots");
87
+ const rawAnnots = page.node.lookup(annotsKey);
88
+ if (!(rawAnnots instanceof PDFArray)) continue;
89
+ const removedRefs = /* @__PURE__ */ new Set();
90
+ const entries = [];
91
+ for (let i = 0; i < rawAnnots.size(); i++) {
92
+ const ref = rawAnnots.get(i);
93
+ const dict = resolveAnnotDict(ref, context);
94
+ entries.push({ ref, dict });
95
+ if (!dict) continue;
96
+ const subtype = getAnnotSubtype(dict);
97
+ if (subtype && ALWAYS_PRESERVE_SUBTYPES.has(subtype)) continue;
98
+ const nm = getAnnotNM(dict);
99
+ const shouldRemove = nm && idsToRemove.has(nm) || refMatchesId(ref, idsToRemove);
100
+ if (shouldRemove && ref instanceof PDFRef) {
101
+ removedRefs.add(ref);
102
+ }
103
+ }
104
+ const kept = [];
105
+ for (const { ref, dict } of entries) {
106
+ if (!(ref instanceof PDFRef)) continue;
107
+ if (!dict) {
108
+ kept.push(ref);
109
+ continue;
110
+ }
111
+ const subtype = getAnnotSubtype(dict);
112
+ if (subtype && ALWAYS_PRESERVE_SUBTYPES.has(subtype)) {
113
+ kept.push(ref);
114
+ continue;
115
+ }
116
+ const nm = getAnnotNM(dict);
117
+ const matched = nm && idsToRemove.has(nm) || refMatchesId(ref, idsToRemove);
118
+ if (matched) continue;
119
+ const irt = getAnnotIRT(dict);
120
+ if (irt instanceof PDFRef && removedRefs.has(irt)) continue;
121
+ kept.push(ref);
122
+ }
123
+ page.node.set(annotsKey, context.obj(kept));
124
+ }
125
+ }
40
126
  async function parseAnnotationToPdf(annotation, page, pdfDoc) {
41
127
  const ParserClass = parserMap[annotation.pdfjsType];
42
128
  if (!ParserClass) {
@@ -111,7 +197,10 @@ export async function exportAnnotationsToPdf({
111
197
  pdfData,
112
198
  annotations,
113
199
  options,
114
- formFieldValues
200
+ formFieldValues,
201
+ originalAnnotationIds,
202
+ modifiedAnnotationIds,
203
+ deletedAnnotationIds
115
204
  }) {
116
205
  const flatten = options?.flatten ?? false;
117
206
  const preserveOriginalAnnotations = options?.preserveOriginalAnnotations ?? false;
@@ -119,8 +208,16 @@ export async function exportAnnotationsToPdf({
119
208
  if (formFieldValues && formFieldValues.length > 0) {
120
209
  await writeFormFieldsToPdf(pdfDoc, formFieldValues);
121
210
  }
211
+ let annotationsToWrite = annotations;
122
212
  if (!preserveOriginalAnnotations) {
123
- clearAllAnnotations(pdfDoc);
213
+ if (originalAnnotationIds && modifiedAnnotationIds && deletedAnnotationIds) {
214
+ removeOwnedAnnotations(pdfDoc, modifiedAnnotationIds, deletedAnnotationIds);
215
+ annotationsToWrite = annotations.filter(
216
+ (a) => !originalAnnotationIds.has(a.id) || modifiedAnnotationIds.has(a.id)
217
+ );
218
+ } else {
219
+ clearAllAnnotations(pdfDoc);
220
+ }
124
221
  }
125
222
  if (flatten) {
126
223
  const pages = pdfDoc.getPages();
@@ -132,7 +229,7 @@ export async function exportAnnotationsToPdf({
132
229
  }
133
230
  } else {
134
231
  const pages = pdfDoc.getPages();
135
- for (const annotation of annotations) {
232
+ for (const annotation of annotationsToWrite) {
136
233
  const page = pages[annotation.pageNumber - 1];
137
234
  if (!page) {
138
235
  console.warn(`Skipping annotation ${annotation.id}; page ${annotation.pageNumber} missing`);
@@ -1,35 +1,123 @@
1
- import { PDFName, PDFNumber, PDFString } from "pdf-lib";
1
+ import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
2
2
  import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
3
3
  import { AnnotationParser } from "./parse.js";
4
4
  const UNKNOWN_USER = "Unknown user";
5
+ function formatPdfNumber(value) {
6
+ if (!Number.isFinite(value)) return "0";
7
+ return Number(value.toFixed(3)).toString();
8
+ }
9
+ function escapePdfString(text) {
10
+ return text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
11
+ }
12
+ function quaddingFromAlign(align) {
13
+ if (align === "center") return 1;
14
+ if (align === "right") return 2;
15
+ return 0;
16
+ }
5
17
  export class FreeTextParser extends AnnotationParser {
6
18
  async parse() {
7
19
  const { annotation, page, pdfDoc } = this;
8
- const [x1, , , y2] = convertKonvaRectToPdfRect(annotation.konvaClientRect, page.getHeight());
9
20
  const context = pdfDoc.context;
10
- const pageWidth = page.getWidth();
11
21
  const pageHeight = page.getHeight();
12
- const iconSize = 20;
13
- const xLeft = Math.max(0, Math.min(x1, pageWidth - iconSize));
14
- const yTop = Math.min(pageHeight, Math.max(y2, iconSize));
15
- const yBottom = yTop - iconSize;
22
+ let konvaGroup;
23
+ try {
24
+ konvaGroup = JSON.parse(annotation.konvaString);
25
+ } catch {
26
+ console.warn(`Invalid konva JSON for FreeText annotation ${annotation.id}`);
27
+ return;
28
+ }
29
+ const textNodes = (konvaGroup.children ?? []).filter((c) => c.className === "Text");
30
+ const textNode = textNodes[0]?.attrs;
31
+ const groupX = konvaGroup.attrs?.x ?? 0;
32
+ const groupY = konvaGroup.attrs?.y ?? 0;
33
+ const scaleX = konvaGroup.attrs?.scaleX ?? 1;
34
+ const scaleY = konvaGroup.attrs?.scaleY ?? 1;
35
+ const text = textNode?.text ?? annotation.contentsObj?.text ?? "";
36
+ const fontSize = (textNode?.fontSize ?? annotation.fontSize ?? 16) * scaleY;
37
+ const color = textNode?.fill ?? annotation.color ?? "#000000";
38
+ const [r, g, b] = rgbToPdfColor(color);
39
+ const align = textNode?.align;
40
+ const quadding = quaddingFromAlign(align);
41
+ const nodeX = groupX + (textNode?.x ?? 0) * scaleX;
42
+ const nodeY = groupY + (textNode?.y ?? 0) * scaleY;
43
+ const lines = text.split("\n");
44
+ const lineHeight = fontSize * 1.2;
45
+ const textHeight = Math.max(lineHeight, lines.length * lineHeight);
46
+ const estimatedCharWidth = fontSize * 0.56;
47
+ const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0);
48
+ const textWidthFromNode = textNode?.width ? textNode.width * scaleX : longestLine * estimatedCharWidth;
49
+ 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;
16
54
  const rect = [
17
- PDFNumber.of(xLeft),
18
- PDFNumber.of(yBottom),
19
- PDFNumber.of(xLeft + iconSize),
20
- PDFNumber.of(yTop)
55
+ PDFNumber.of(pdfX1),
56
+ PDFNumber.of(pdfY1),
57
+ PDFNumber.of(pdfX2),
58
+ PDFNumber.of(pdfY2)
21
59
  ];
60
+ const appearanceWidth = Math.max(1, pdfX2 - pdfX1);
61
+ const appearanceHeight = Math.max(1, pdfY2 - pdfY1);
62
+ const commands = ["q"];
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const line = lines[i] ?? "";
65
+ if (line.length === 0 && i < lines.length - 1) continue;
66
+ const lineY = appearanceHeight - (i + 0.8) * lineHeight;
67
+ let lineX = 0;
68
+ if (quadding === 1) {
69
+ const lineWidth = line.length * estimatedCharWidth;
70
+ lineX = Math.max(0, (appearanceWidth - lineWidth) / 2);
71
+ } else if (quadding === 2) {
72
+ const lineWidth = line.length * estimatedCharWidth;
73
+ lineX = Math.max(0, appearanceWidth - lineWidth);
74
+ }
75
+ commands.push(
76
+ "BT",
77
+ `/Helvetica ${formatPdfNumber(fontSize)} Tf`,
78
+ `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`,
79
+ `${formatPdfNumber(lineX)} ${formatPdfNumber(lineY)} Td`,
80
+ `(${escapePdfString(line)}) Tj`,
81
+ "ET"
82
+ );
83
+ }
84
+ commands.push("Q");
85
+ const fontDict = context.obj({
86
+ Type: PDFName.of("Font"),
87
+ Subtype: PDFName.of("Type1"),
88
+ BaseFont: PDFName.of("Helvetica"),
89
+ Encoding: PDFName.of("WinAnsiEncoding")
90
+ });
91
+ const fontRef = context.register(fontDict);
92
+ const appearanceStream = PDFRawStream.of(
93
+ context.obj({
94
+ Type: PDFName.of("XObject"),
95
+ Subtype: PDFName.of("Form"),
96
+ BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
97
+ Resources: context.obj({
98
+ Font: context.obj({ Helvetica: fontRef })
99
+ })
100
+ }),
101
+ new TextEncoder().encode(commands.join("\n"))
102
+ );
103
+ const appearanceRef = context.register(appearanceStream);
104
+ const ap = context.obj({ N: appearanceRef });
105
+ const da = `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg /Helvetica ${formatPdfNumber(fontSize)} Tf`;
22
106
  const mainAnn = context.obj({
23
107
  Type: PDFName.of("Annot"),
24
- Subtype: PDFName.of("Text"),
108
+ Subtype: PDFName.of("FreeText"),
25
109
  Rect: rect,
26
110
  NM: PDFString.of(annotation.id),
27
- Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
28
- Name: PDFName.of("Comment"),
111
+ Contents: stringToPDFHexString(text),
112
+ DA: PDFString.of(da),
113
+ Q: PDFNumber.of(quadding),
29
114
  T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
30
115
  M: PDFString.of(annotation.date || ""),
31
- C: rgbToPdfColor(annotation.color || void 0),
32
- Open: false
116
+ C: context.obj(rgbToPdfColor(annotation.color || void 0)),
117
+ Border: context.obj([0, 0, 0]),
118
+ F: PDFNumber.of(4),
119
+ // Print flag
120
+ AP: ap
33
121
  });
34
122
  const mainAnnRef = context.register(mainAnn);
35
123
  this.addAnnotationToPage(page, mainAnnRef);
@@ -37,11 +125,11 @@ export class FreeTextParser extends AnnotationParser {
37
125
  const replyAnn = context.obj({
38
126
  Type: PDFName.of("Annot"),
39
127
  Subtype: PDFName.of("Text"),
40
- Rect: rect,
128
+ Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight),
41
129
  Contents: stringToPDFHexString(comment.content),
42
130
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
43
131
  M: PDFString.of(comment.date || ""),
44
- C: rgbToPdfColor(annotation.color || void 0),
132
+ C: context.obj(rgbToPdfColor(annotation.color || void 0)),
45
133
  IRT: mainAnnRef,
46
134
  RT: PDFName.of("R"),
47
135
  NM: PDFString.of(comment.id),
@@ -56,9 +56,26 @@ export class HighlightParser extends AnnotationParser {
56
56
  }];
57
57
  }
58
58
  const quadPoints = buildQuadPoints(highlightRects, pageHeight);
59
- const rect = convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight);
60
59
  const color = rgbToPdfColor(annotation.color || void 0);
61
- const [x1, y1, x2, y2] = rect;
60
+ let minX = Infinity;
61
+ let minY = Infinity;
62
+ let maxX = -Infinity;
63
+ 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;
73
+ }
74
+ const x1 = minX;
75
+ const y1 = minY;
76
+ const x2 = maxX;
77
+ const y2 = maxY;
78
+ const rect = [x1, y1, x2, y2];
62
79
  const appearanceWidth = Math.max(1, x2 - x1);
63
80
  const appearanceHeight = Math.max(1, y2 - y1);
64
81
  const [r, g, b] = color;
@@ -87,9 +104,8 @@ export class HighlightParser extends AnnotationParser {
87
104
  ExtGState: context.obj({
88
105
  GS1: context.obj({
89
106
  Type: PDFName.of("ExtGState"),
90
- ca: PDFNumber.of(0.35),
91
- CA: PDFNumber.of(0.35),
92
- BM: PDFName.of("Multiply")
107
+ ca: PDFNumber.of(0.5),
108
+ CA: PDFNumber.of(0.5)
93
109
  })
94
110
  })
95
111
  })
@@ -108,7 +124,7 @@ export class HighlightParser extends AnnotationParser {
108
124
  Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
109
125
  M: PDFString.of(annotation.date || ""),
110
126
  NM: PDFString.of(annotation.id),
111
- CA: PDFNumber.of(0.35),
127
+ CA: PDFNumber.of(0.5),
112
128
  AP: ap
113
129
  });
114
130
  const mainAnnRef = context.register(mainAnn);
@@ -51,12 +51,32 @@ export class InkParser extends AnnotationParser {
51
51
  W: PDFNumber.of(strokeWidth),
52
52
  S: PDFName.of("S")
53
53
  });
54
- const rect = convertKonvaRectToPdfRect(annotation.konvaClientRect, pageHeight);
55
- const [x1, y1, x2, y2] = rect;
54
+ const pad = Math.max(strokeWidth, 1);
55
+ let minX = Infinity;
56
+ let minY = Infinity;
57
+ let maxX = -Infinity;
58
+ let maxY = -Infinity;
59
+ for (const path of transformedPaths) {
60
+ for (let i = 0; i < path.length; i += 2) {
61
+ const px = path[i];
62
+ const py = path[i + 1];
63
+ if (px < minX) minX = px;
64
+ if (py < minY) minY = py;
65
+ if (px > maxX) maxX = px;
66
+ if (py > maxY) maxY = py;
67
+ }
68
+ }
69
+ const x1 = minX - pad;
70
+ const y1 = minY - pad;
71
+ const x2 = maxX + pad;
72
+ const y2 = maxY + pad;
73
+ const rect = [x1, y1, x2, y2];
56
74
  const appearanceWidth = Math.max(1, x2 - x1);
57
75
  const appearanceHeight = Math.max(1, y2 - y1);
76
+ const useExtGState = opacity !== 1;
58
77
  const commands = [
59
78
  "q",
79
+ ...useExtGState ? ["/GS1 gs"] : [],
60
80
  `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} RG`,
61
81
  `${formatPdfNumber(Math.max(0.1, strokeWidth))} w`,
62
82
  "1 J",
@@ -75,12 +95,21 @@ export class InkParser extends AnnotationParser {
75
95
  commands.push("S");
76
96
  }
77
97
  commands.push("Q");
98
+ const resources = useExtGState ? context.obj({
99
+ ExtGState: context.obj({
100
+ GS1: context.obj({
101
+ Type: PDFName.of("ExtGState"),
102
+ CA: PDFNumber.of(opacity),
103
+ ca: PDFNumber.of(opacity)
104
+ })
105
+ })
106
+ }) : context.obj({});
78
107
  const appearanceStream = PDFRawStream.of(
79
108
  context.obj({
80
109
  Type: PDFName.of("XObject"),
81
110
  Subtype: PDFName.of("Form"),
82
111
  BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
83
- Resources: context.obj({})
112
+ Resources: resources
84
113
  }),
85
114
  new TextEncoder().encode(commands.join("\n"))
86
115
  );