kviewer 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/module.json +2 -2
- package/dist/runtime/annotation/engine/painter.d.ts +7 -0
- package/dist/runtime/annotation/engine/painter.js +14 -0
- package/dist/runtime/annotation/engine/store.d.ts +5 -0
- package/dist/runtime/annotation/engine/store.js +17 -0
- package/dist/runtime/annotation/engine/tools/free-text.js +3 -1
- package/dist/runtime/annotation/engine/types.d.ts +20 -0
- package/dist/runtime/annotation/pdf-export/export.d.ts +4 -1
- package/dist/runtime/annotation/pdf-export/export.js +101 -4
- package/dist/runtime/annotation/pdf-export/parse_freetext.js +106 -18
- package/dist/runtime/annotation/pdf-export/parse_highlight.js +22 -6
- package/dist/runtime/annotation/pdf-export/parse_ink.js +32 -3
- package/dist/runtime/annotation/pdf-export/parse_line.js +133 -13
- package/dist/runtime/annotation/pdf-import/decode.d.ts +21 -0
- package/dist/runtime/annotation/pdf-import/decode.js +134 -0
- package/dist/runtime/annotation/pdf-import/decode_circle.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_circle.js +36 -0
- package/dist/runtime/annotation/pdf-import/decode_freetext.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_freetext.js +87 -0
- package/dist/runtime/annotation/pdf-import/decode_highlight.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_highlight.js +96 -0
- package/dist/runtime/annotation/pdf-import/decode_ink.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_ink.js +48 -0
- package/dist/runtime/annotation/pdf-import/decode_line.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_line.js +48 -0
- package/dist/runtime/annotation/pdf-import/decode_square.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_square.js +39 -0
- package/dist/runtime/annotation/pdf-import/decode_stamp.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_stamp.js +38 -0
- package/dist/runtime/annotation/pdf-import/decode_text.d.ts +3 -0
- package/dist/runtime/annotation/pdf-import/decode_text.js +33 -0
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +23 -0
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +168 -0
- package/dist/runtime/annotation/pdf-import/types.d.ts +57 -0
- package/dist/runtime/annotation/pdf-import/types.js +25 -0
- package/dist/runtime/annotation/pdf-import/utils.d.ts +48 -0
- package/dist/runtime/annotation/pdf-import/utils.js +250 -0
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/AnnotationToolbar.vue +1 -0
- package/dist/runtime/components/FloatingPageIndicator.vue +4 -1
- package/dist/runtime/components/PdfPage.vue +27 -1
- package/dist/runtime/components/Viewer.d.vue.ts +12 -1
- package/dist/runtime/components/Viewer.vue +112 -34
- package/dist/runtime/components/Viewer.vue.d.ts +12 -1
- package/dist/runtime/components/ViewerBar.vue +10 -5
- package/dist/runtime/components/ViewerTabs.d.vue.ts +16 -1
- package/dist/runtime/components/ViewerTabs.vue +42 -12
- package/dist/runtime/components/ViewerTabs.vue.d.ts +16 -1
- package/dist/runtime/components/form-fields/FormCheckbox.vue +37 -1
- package/dist/runtime/components/tools/ActionTools.vue +3 -0
- package/dist/runtime/components/tools/PageInfo.vue +1 -1
- package/dist/runtime/components/tools/SearchTool.vue +3 -1
- package/dist/runtime/components/tools/ZoomControls.vue +3 -0
- package/dist/runtime/composables/shape-detection-utils.d.ts +38 -0
- package/dist/runtime/composables/shape-detection-utils.js +50 -0
- package/dist/runtime/composables/useFormFields.d.ts +3 -1
- package/dist/runtime/composables/useFormFields.js +47 -0
- package/dist/runtime/composables/useShapeDetection.d.ts +24 -0
- package/dist/runtime/composables/useShapeDetection.js +235 -0
- package/dist/runtime/composables/useViewerState.d.ts +2 -0
- package/dist/runtime/composables/useViewerState.js +5 -1
- package/dist/runtime/plugin.d.ts +1 -1
- package/package.json +28 -5
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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(
|
|
18
|
-
PDFNumber.of(
|
|
19
|
-
PDFNumber.of(
|
|
20
|
-
PDFNumber.of(
|
|
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("
|
|
108
|
+
Subtype: PDFName.of("FreeText"),
|
|
25
109
|
Rect: rect,
|
|
26
110
|
NM: PDFString.of(annotation.id),
|
|
27
|
-
Contents: stringToPDFHexString(
|
|
28
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
91
|
-
CA: PDFNumber.of(0.
|
|
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.
|
|
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
|
|
55
|
-
|
|
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:
|
|
112
|
+
Resources: resources
|
|
84
113
|
}),
|
|
85
114
|
new TextEncoder().encode(commands.join("\n"))
|
|
86
115
|
);
|