kviewer 0.0.4 → 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 +8 -3
- 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
|
@@ -1,7 +1,11 @@
|
|
|
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
|
+
}
|
|
5
9
|
export class LineParser extends AnnotationParser {
|
|
6
10
|
async parse() {
|
|
7
11
|
const { annotation, page, pdfDoc } = this;
|
|
@@ -13,17 +17,24 @@ export class LineParser extends AnnotationParser {
|
|
|
13
17
|
const groupY = konvaGroup.attrs?.y || 0;
|
|
14
18
|
const scaleX = konvaGroup.attrs?.scaleX || 1;
|
|
15
19
|
const scaleY = konvaGroup.attrs?.scaleY || 1;
|
|
20
|
+
const transformedPaths = [];
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const points = line.attrs?.points ?? [];
|
|
23
|
+
if (points.length < 4) continue;
|
|
24
|
+
const transformedPoints = [];
|
|
25
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
26
|
+
const x = groupX + (points[i] ?? 0) * scaleX;
|
|
27
|
+
const y = groupY + (points[i + 1] ?? 0) * scaleY;
|
|
28
|
+
transformedPoints.push(x, pageHeight - y);
|
|
29
|
+
}
|
|
30
|
+
transformedPaths.push(transformedPoints);
|
|
31
|
+
}
|
|
32
|
+
if (transformedPaths.length === 0) {
|
|
33
|
+
console.warn(`Line annotation ${annotation.id} has no drawable paths`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
16
36
|
const inkList = context.obj(
|
|
17
|
-
|
|
18
|
-
const points = line.attrs?.points ?? [];
|
|
19
|
-
const transformedPoints = [];
|
|
20
|
-
for (let i = 0; i < points.length; i += 2) {
|
|
21
|
-
const x = groupX + points[i] * scaleX;
|
|
22
|
-
const y = groupY + points[i + 1] * scaleY;
|
|
23
|
-
transformedPoints.push(x, pageHeight - y);
|
|
24
|
-
}
|
|
25
|
-
return context.obj(transformedPoints);
|
|
26
|
-
})
|
|
37
|
+
transformedPaths.map((path) => context.obj(path))
|
|
27
38
|
);
|
|
28
39
|
const firstLine = lines[0]?.attrs || {};
|
|
29
40
|
const strokeWidth = firstLine.strokeWidth ?? 1;
|
|
@@ -34,10 +45,118 @@ export class LineParser extends AnnotationParser {
|
|
|
34
45
|
W: PDFNumber.of(strokeWidth),
|
|
35
46
|
S: PDFName.of("S")
|
|
36
47
|
});
|
|
48
|
+
const pointerLength = firstLine.pointerLength ?? 10;
|
|
49
|
+
const pad = Math.max(strokeWidth, pointerLength, 1);
|
|
50
|
+
let minX = Infinity;
|
|
51
|
+
let minY = Infinity;
|
|
52
|
+
let maxX = -Infinity;
|
|
53
|
+
let maxY = -Infinity;
|
|
54
|
+
for (const path of transformedPaths) {
|
|
55
|
+
for (let i = 0; i < path.length; i += 2) {
|
|
56
|
+
const px = path[i];
|
|
57
|
+
const py = path[i + 1];
|
|
58
|
+
if (px < minX) minX = px;
|
|
59
|
+
if (py < minY) minY = py;
|
|
60
|
+
if (px > maxX) maxX = px;
|
|
61
|
+
if (py > maxY) maxY = py;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const x1 = minX - pad;
|
|
65
|
+
const y1 = minY - pad;
|
|
66
|
+
const x2 = maxX + pad;
|
|
67
|
+
const y2 = maxY + pad;
|
|
68
|
+
const rect = [x1, y1, x2, y2];
|
|
69
|
+
const appearanceWidth = Math.max(1, x2 - x1);
|
|
70
|
+
const appearanceHeight = Math.max(1, y2 - y1);
|
|
71
|
+
const useExtGState = opacity !== 1;
|
|
72
|
+
const commands = [
|
|
73
|
+
"q",
|
|
74
|
+
...useExtGState ? ["/GS1 gs"] : [],
|
|
75
|
+
`${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} RG`,
|
|
76
|
+
`${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`,
|
|
77
|
+
`${formatPdfNumber(Math.max(0.1, strokeWidth))} w`,
|
|
78
|
+
"1 J",
|
|
79
|
+
// round line cap
|
|
80
|
+
"1 j"
|
|
81
|
+
// round line join
|
|
82
|
+
];
|
|
83
|
+
for (const path of transformedPaths) {
|
|
84
|
+
if (path.length < 4) continue;
|
|
85
|
+
commands.push(
|
|
86
|
+
`${formatPdfNumber((path[0] ?? 0) - x1)} ${formatPdfNumber((path[1] ?? 0) - y1)} m`
|
|
87
|
+
);
|
|
88
|
+
for (let i = 2; i < path.length; i += 2) {
|
|
89
|
+
commands.push(
|
|
90
|
+
`${formatPdfNumber((path[i] ?? 0) - x1)} ${formatPdfNumber((path[i + 1] ?? 0) - y1)} l`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
commands.push("S");
|
|
94
|
+
const pointerLength2 = firstLine.pointerLength ?? 10;
|
|
95
|
+
const pointerWidth = firstLine.pointerWidth ?? 10;
|
|
96
|
+
const hasEndArrow = firstLine.pointerAtEnding !== false;
|
|
97
|
+
if (hasEndArrow && path.length >= 4) {
|
|
98
|
+
const endX = (path[path.length - 2] ?? 0) - x1;
|
|
99
|
+
const endY = (path[path.length - 1] ?? 0) - y1;
|
|
100
|
+
const prevX = (path[path.length - 4] ?? 0) - x1;
|
|
101
|
+
const prevY = (path[path.length - 3] ?? 0) - y1;
|
|
102
|
+
const angle = Math.atan2(endY - prevY, endX - prevX);
|
|
103
|
+
const halfWidth = pointerWidth / 2;
|
|
104
|
+
const leftX = endX - pointerLength2 * Math.cos(angle) + halfWidth * Math.sin(angle);
|
|
105
|
+
const leftY = endY - pointerLength2 * Math.sin(angle) - halfWidth * Math.cos(angle);
|
|
106
|
+
const rightX = endX - pointerLength2 * Math.cos(angle) - halfWidth * Math.sin(angle);
|
|
107
|
+
const rightY = endY - pointerLength2 * Math.sin(angle) + halfWidth * Math.cos(angle);
|
|
108
|
+
commands.push(
|
|
109
|
+
`${formatPdfNumber(endX)} ${formatPdfNumber(endY)} m`,
|
|
110
|
+
`${formatPdfNumber(leftX)} ${formatPdfNumber(leftY)} l`,
|
|
111
|
+
`${formatPdfNumber(rightX)} ${formatPdfNumber(rightY)} l`,
|
|
112
|
+
"f"
|
|
113
|
+
// fill the arrowhead
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (firstLine.pointerAtBeginning && path.length >= 4) {
|
|
117
|
+
const startX = (path[0] ?? 0) - x1;
|
|
118
|
+
const startY = (path[1] ?? 0) - y1;
|
|
119
|
+
const nextX = (path[2] ?? 0) - x1;
|
|
120
|
+
const nextY = (path[3] ?? 0) - y1;
|
|
121
|
+
const angle = Math.atan2(startY - nextY, startX - nextX);
|
|
122
|
+
const halfWidth = pointerWidth / 2;
|
|
123
|
+
const leftX = startX - pointerLength2 * Math.cos(angle) + halfWidth * Math.sin(angle);
|
|
124
|
+
const leftY = startX - pointerLength2 * Math.sin(angle) - halfWidth * Math.cos(angle);
|
|
125
|
+
const rightX = startX - pointerLength2 * Math.cos(angle) - halfWidth * Math.sin(angle);
|
|
126
|
+
const rightY = startX - pointerLength2 * Math.sin(angle) + halfWidth * Math.cos(angle);
|
|
127
|
+
commands.push(
|
|
128
|
+
`${formatPdfNumber(startX)} ${formatPdfNumber(startY)} m`,
|
|
129
|
+
`${formatPdfNumber(leftX)} ${formatPdfNumber(leftY)} l`,
|
|
130
|
+
`${formatPdfNumber(rightX)} ${formatPdfNumber(rightY)} l`,
|
|
131
|
+
"f"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
commands.push("Q");
|
|
136
|
+
const resources = useExtGState ? context.obj({
|
|
137
|
+
ExtGState: context.obj({
|
|
138
|
+
GS1: context.obj({
|
|
139
|
+
Type: PDFName.of("ExtGState"),
|
|
140
|
+
CA: PDFNumber.of(opacity),
|
|
141
|
+
ca: PDFNumber.of(opacity)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
}) : context.obj({});
|
|
145
|
+
const appearanceStream = PDFRawStream.of(
|
|
146
|
+
context.obj({
|
|
147
|
+
Type: PDFName.of("XObject"),
|
|
148
|
+
Subtype: PDFName.of("Form"),
|
|
149
|
+
BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
|
|
150
|
+
Resources: resources
|
|
151
|
+
}),
|
|
152
|
+
new TextEncoder().encode(commands.join("\n"))
|
|
153
|
+
);
|
|
154
|
+
const appearanceRef = context.register(appearanceStream);
|
|
155
|
+
const ap = context.obj({ N: appearanceRef });
|
|
37
156
|
const mainAnn = context.obj({
|
|
38
157
|
Type: PDFName.of("Annot"),
|
|
39
158
|
Subtype: PDFName.of("Ink"),
|
|
40
|
-
Rect:
|
|
159
|
+
Rect: rect,
|
|
41
160
|
InkList: inkList,
|
|
42
161
|
C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
|
|
43
162
|
T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
|
|
@@ -46,7 +165,8 @@ export class LineParser extends AnnotationParser {
|
|
|
46
165
|
NM: PDFString.of(annotation.id),
|
|
47
166
|
Border: context.obj([0, 0, 0]),
|
|
48
167
|
BS: bs,
|
|
49
|
-
CA: PDFNumber.of(opacity)
|
|
168
|
+
CA: PDFNumber.of(opacity),
|
|
169
|
+
AP: ap
|
|
50
170
|
});
|
|
51
171
|
const mainAnnRef = context.register(mainAnn);
|
|
52
172
|
this.addAnnotationToPage(page, mainAnnRef);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
2
|
+
import { type IAnnotationStore } from '../engine/types.js';
|
|
3
|
+
import { type StampAppearance } from './types.js';
|
|
4
|
+
export interface DecodePdfAnnotationsResult {
|
|
5
|
+
annotations: IAnnotationStore[];
|
|
6
|
+
skipped: number;
|
|
7
|
+
errors: number;
|
|
8
|
+
pageErrors: number;
|
|
9
|
+
stampExtracted: number;
|
|
10
|
+
stampSkipped: number;
|
|
11
|
+
}
|
|
12
|
+
interface DecodePageOptions {
|
|
13
|
+
stampAppearances?: Map<string, StampAppearance>;
|
|
14
|
+
}
|
|
15
|
+
export declare function filterCollidingAnnotations(annotations: IAnnotationStore[], existingIds: Set<string>): {
|
|
16
|
+
annotations: IAnnotationStore[];
|
|
17
|
+
skipped: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function decodePdfPageAnnotations(pageAnnotations: unknown[], pageNumber: number, pageHeight: number, options?: DecodePageOptions): DecodePdfAnnotationsResult;
|
|
20
|
+
export declare function decodePdfAnnotationsFromDocument(pdfDoc: PDFDocumentProxy): Promise<DecodePdfAnnotationsResult>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { PdfjsAnnotationType } from "../engine/types.js";
|
|
2
|
+
import { decodeCircleAnnotation } from "./decode_circle.js";
|
|
3
|
+
import { decodeFreeTextAnnotation } from "./decode_freetext.js";
|
|
4
|
+
import { decodeHighlightAnnotation } from "./decode_highlight.js";
|
|
5
|
+
import { decodeInkAnnotation } from "./decode_ink.js";
|
|
6
|
+
import { decodeLineAnnotation } from "./decode_line.js";
|
|
7
|
+
import { decodeStampAnnotation } from "./decode_stamp.js";
|
|
8
|
+
import { decodeSquareAnnotation } from "./decode_square.js";
|
|
9
|
+
import { decodeTextAnnotation } from "./decode_text.js";
|
|
10
|
+
import { extractStampAppearancesForPage } from "./extract_stamp_appearance.js";
|
|
11
|
+
import { isRawPdfAnnotation } from "./types.js";
|
|
12
|
+
const decoderMap = {
|
|
13
|
+
[PdfjsAnnotationType.TEXT]: decodeTextAnnotation,
|
|
14
|
+
[PdfjsAnnotationType.FREETEXT]: decodeFreeTextAnnotation,
|
|
15
|
+
[PdfjsAnnotationType.HIGHLIGHT]: decodeHighlightAnnotation,
|
|
16
|
+
[PdfjsAnnotationType.UNDERLINE]: decodeHighlightAnnotation,
|
|
17
|
+
[PdfjsAnnotationType.STRIKEOUT]: decodeHighlightAnnotation,
|
|
18
|
+
[PdfjsAnnotationType.SQUARE]: decodeSquareAnnotation,
|
|
19
|
+
[PdfjsAnnotationType.CIRCLE]: decodeCircleAnnotation,
|
|
20
|
+
[PdfjsAnnotationType.INK]: decodeInkAnnotation,
|
|
21
|
+
[PdfjsAnnotationType.LINE]: decodeLineAnnotation,
|
|
22
|
+
[PdfjsAnnotationType.STAMP]: decodeStampAnnotation
|
|
23
|
+
};
|
|
24
|
+
export function filterCollidingAnnotations(annotations, existingIds) {
|
|
25
|
+
const result = [];
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
for (const annotation of annotations) {
|
|
28
|
+
if (existingIds.has(annotation.id)) {
|
|
29
|
+
skipped++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
existingIds.add(annotation.id);
|
|
33
|
+
result.push(annotation);
|
|
34
|
+
}
|
|
35
|
+
return { annotations: result, skipped };
|
|
36
|
+
}
|
|
37
|
+
export function decodePdfPageAnnotations(pageAnnotations, pageNumber, pageHeight, options = {}) {
|
|
38
|
+
const annotations = [];
|
|
39
|
+
let skipped = 0;
|
|
40
|
+
let errors = 0;
|
|
41
|
+
const rawAnnotations = pageAnnotations.filter(isRawPdfAnnotation);
|
|
42
|
+
skipped += pageAnnotations.length - rawAnnotations.length;
|
|
43
|
+
const context = {
|
|
44
|
+
pageNumber,
|
|
45
|
+
pageHeight,
|
|
46
|
+
allAnnotations: rawAnnotations,
|
|
47
|
+
stampAppearances: options.stampAppearances
|
|
48
|
+
};
|
|
49
|
+
const stampEntries = rawAnnotations.filter(
|
|
50
|
+
(annotation) => annotation.annotationType === PdfjsAnnotationType.STAMP && typeof annotation.id === "string"
|
|
51
|
+
);
|
|
52
|
+
const stampExtracted = Math.min(stampEntries.length, options.stampAppearances?.size ?? 0);
|
|
53
|
+
const stampSkipped = Math.max(0, stampEntries.length - stampExtracted);
|
|
54
|
+
for (const annotation of rawAnnotations) {
|
|
55
|
+
const decoder = decoderMap[annotation.annotationType];
|
|
56
|
+
if (!decoder) {
|
|
57
|
+
skipped++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const decoded = decoder(annotation, context);
|
|
62
|
+
if (decoded) {
|
|
63
|
+
annotations.push(decoded);
|
|
64
|
+
} else {
|
|
65
|
+
skipped++;
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
errors++;
|
|
69
|
+
skipped++;
|
|
70
|
+
console.warn(`[KViewer] Failed to decode annotation ${annotation.id}`, error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
annotations,
|
|
75
|
+
skipped,
|
|
76
|
+
errors,
|
|
77
|
+
pageErrors: 0,
|
|
78
|
+
stampExtracted,
|
|
79
|
+
stampSkipped
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export async function decodePdfAnnotationsFromDocument(pdfDoc) {
|
|
83
|
+
const annotations = [];
|
|
84
|
+
let skipped = 0;
|
|
85
|
+
let errors = 0;
|
|
86
|
+
let pageErrors = 0;
|
|
87
|
+
let stampExtracted = 0;
|
|
88
|
+
let stampSkipped = 0;
|
|
89
|
+
for (let pageNumber = 1; pageNumber <= pdfDoc.numPages; pageNumber++) {
|
|
90
|
+
try {
|
|
91
|
+
const page = await pdfDoc.getPage(pageNumber);
|
|
92
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
93
|
+
const pageAnnotations = await page.getAnnotations();
|
|
94
|
+
const rawAnnotations = pageAnnotations.filter(isRawPdfAnnotation);
|
|
95
|
+
const hasStampAnnotations = rawAnnotations.some(
|
|
96
|
+
(annotation) => annotation.annotationType === PdfjsAnnotationType.STAMP && typeof annotation.id === "string"
|
|
97
|
+
);
|
|
98
|
+
let stampAppearances;
|
|
99
|
+
if (hasStampAnnotations) {
|
|
100
|
+
try {
|
|
101
|
+
stampAppearances = await extractStampAppearancesForPage(
|
|
102
|
+
page,
|
|
103
|
+
viewport.height,
|
|
104
|
+
rawAnnotations
|
|
105
|
+
);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn(`[KViewer] Failed to extract stamp appearances on page ${pageNumber}`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const decoded = decodePdfPageAnnotations(
|
|
111
|
+
pageAnnotations,
|
|
112
|
+
pageNumber,
|
|
113
|
+
viewport.height,
|
|
114
|
+
{ stampAppearances }
|
|
115
|
+
);
|
|
116
|
+
annotations.push(...decoded.annotations);
|
|
117
|
+
skipped += decoded.skipped;
|
|
118
|
+
errors += decoded.errors;
|
|
119
|
+
stampExtracted += decoded.stampExtracted;
|
|
120
|
+
stampSkipped += decoded.stampSkipped;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
pageErrors++;
|
|
123
|
+
console.warn(`[KViewer] Failed to decode page ${pageNumber} annotations`, error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
annotations,
|
|
128
|
+
skipped,
|
|
129
|
+
errors,
|
|
130
|
+
pageErrors,
|
|
131
|
+
stampExtracted,
|
|
132
|
+
stampSkipped
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import { AnnotationType } from "../engine/types.js";
|
|
3
|
+
import {
|
|
4
|
+
createAnnotationStore,
|
|
5
|
+
createGhostGroup,
|
|
6
|
+
getAnnotationColor,
|
|
7
|
+
normalizedStrokeWidth,
|
|
8
|
+
rectFromPdfRect
|
|
9
|
+
} from "./utils.js";
|
|
10
|
+
export function decodeCircleAnnotation(annotation, context) {
|
|
11
|
+
const rect = rectFromPdfRect(annotation.rect, context.pageHeight);
|
|
12
|
+
if (!rect || typeof annotation.id !== "string") return null;
|
|
13
|
+
const color = getAnnotationColor(annotation, AnnotationType.CIRCLE);
|
|
14
|
+
const strokeWidth = normalizedStrokeWidth(annotation, 1);
|
|
15
|
+
const group = createGhostGroup(annotation.id);
|
|
16
|
+
group.add(new Konva.Ellipse({
|
|
17
|
+
x: rect.x + rect.width / 2,
|
|
18
|
+
y: rect.y + rect.height / 2,
|
|
19
|
+
radiusX: rect.width / 2,
|
|
20
|
+
radiusY: rect.height / 2,
|
|
21
|
+
strokeScaleEnabled: false,
|
|
22
|
+
strokeWidth,
|
|
23
|
+
stroke: color
|
|
24
|
+
}));
|
|
25
|
+
const store = createAnnotationStore({
|
|
26
|
+
annotation,
|
|
27
|
+
allAnnotations: context.allAnnotations,
|
|
28
|
+
pageNumber: context.pageNumber,
|
|
29
|
+
type: AnnotationType.CIRCLE,
|
|
30
|
+
group,
|
|
31
|
+
color,
|
|
32
|
+
overrideSubtype: "Circle"
|
|
33
|
+
});
|
|
34
|
+
group.destroy();
|
|
35
|
+
return store;
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import { AnnotationType } from "../engine/types.js";
|
|
3
|
+
import { asAppearanceData, isNumber } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
colorArrayToRgb,
|
|
6
|
+
createAnnotationStore,
|
|
7
|
+
createGhostGroup,
|
|
8
|
+
getFreeTextContent,
|
|
9
|
+
getAnnotationColor,
|
|
10
|
+
localPointFromPdfPoint,
|
|
11
|
+
rectFromPdfRect
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
function estimatedTextWidth(text, fontSize) {
|
|
14
|
+
const lines = text.split("\n");
|
|
15
|
+
const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
16
|
+
if (longestLine === 0) return 0;
|
|
17
|
+
return longestLine * fontSize * 0.56;
|
|
18
|
+
}
|
|
19
|
+
function measuredTextWidth(text, fontSize, fontFamily) {
|
|
20
|
+
const measureNode = new Konva.Text({
|
|
21
|
+
text,
|
|
22
|
+
fontSize,
|
|
23
|
+
fontFamily
|
|
24
|
+
});
|
|
25
|
+
const widthFn = measureNode.width;
|
|
26
|
+
const measured = typeof widthFn === "function" ? widthFn.call(measureNode) : null;
|
|
27
|
+
const destroyFn = measureNode.destroy;
|
|
28
|
+
if (typeof destroyFn === "function") {
|
|
29
|
+
destroyFn.call(measureNode);
|
|
30
|
+
}
|
|
31
|
+
return isNumber(measured) ? measured : estimatedTextWidth(text, fontSize);
|
|
32
|
+
}
|
|
33
|
+
function inferTextAlign(rectWidth, textPositionX, measuredWidth) {
|
|
34
|
+
if (!isNumber(textPositionX) || rectWidth <= 0) return "left";
|
|
35
|
+
const leftGap = textPositionX;
|
|
36
|
+
const rightGap = rectWidth - (leftGap + measuredWidth);
|
|
37
|
+
const tolerance = Math.max(1, rectWidth * 0.08);
|
|
38
|
+
if (leftGap > tolerance && rightGap > tolerance && Math.abs(leftGap - rightGap) <= tolerance * 2) {
|
|
39
|
+
return "center";
|
|
40
|
+
}
|
|
41
|
+
if (rightGap <= tolerance && leftGap > tolerance) {
|
|
42
|
+
return "right";
|
|
43
|
+
}
|
|
44
|
+
return "left";
|
|
45
|
+
}
|
|
46
|
+
export function decodeFreeTextAnnotation(annotation, context) {
|
|
47
|
+
const rect = rectFromPdfRect(annotation.rect, context.pageHeight);
|
|
48
|
+
if (!rect || typeof annotation.id !== "string") return null;
|
|
49
|
+
const appearance = asAppearanceData(annotation.defaultAppearanceData);
|
|
50
|
+
const color = colorArrayToRgb(appearance?.fontColor) ?? getAnnotationColor(annotation, AnnotationType.FREETEXT);
|
|
51
|
+
const rawFontSize = appearance?.fontSize;
|
|
52
|
+
const fontSize = isNumber(rawFontSize) ? Math.max(1, rawFontSize) : 18;
|
|
53
|
+
const fontFamily = typeof appearance?.fontName === "string" && appearance.fontName.length > 0 ? appearance.fontName : void 0;
|
|
54
|
+
const text = getFreeTextContent(annotation);
|
|
55
|
+
const textPosition = localPointFromPdfPoint(annotation.textPosition);
|
|
56
|
+
const textWidth = measuredTextWidth(text, fontSize, fontFamily);
|
|
57
|
+
const textAlign = inferTextAlign(rect.width, textPosition?.x ?? null, textWidth);
|
|
58
|
+
const x = textPosition && textAlign === "left" ? rect.x + Math.max(0, textPosition.x) : rect.x;
|
|
59
|
+
const y = textPosition ? rect.y + rect.height - textPosition.y - fontSize : rect.y + 2;
|
|
60
|
+
const width = rect.width > 0 ? textPosition && textAlign === "left" ? Math.max(1, rect.width - Math.max(0, textPosition.x)) : rect.width : void 0;
|
|
61
|
+
const group = createGhostGroup(annotation.id);
|
|
62
|
+
const textNode = new Konva.Text({
|
|
63
|
+
x,
|
|
64
|
+
y,
|
|
65
|
+
text,
|
|
66
|
+
align: textAlign,
|
|
67
|
+
fontSize,
|
|
68
|
+
fontFamily,
|
|
69
|
+
fill: color,
|
|
70
|
+
width,
|
|
71
|
+
wrap: width ? "word" : "none"
|
|
72
|
+
});
|
|
73
|
+
group.add(textNode);
|
|
74
|
+
const store = createAnnotationStore({
|
|
75
|
+
annotation,
|
|
76
|
+
allAnnotations: context.allAnnotations,
|
|
77
|
+
pageNumber: context.pageNumber,
|
|
78
|
+
type: AnnotationType.FREETEXT,
|
|
79
|
+
group,
|
|
80
|
+
color,
|
|
81
|
+
fontSize,
|
|
82
|
+
contentsText: text,
|
|
83
|
+
overrideSubtype: "FreeText"
|
|
84
|
+
});
|
|
85
|
+
group.destroy();
|
|
86
|
+
return store;
|
|
87
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import { AnnotationType, PdfjsAnnotationType } from "../engine/types.js";
|
|
3
|
+
import {
|
|
4
|
+
createAnnotationStore,
|
|
5
|
+
createGhostGroup,
|
|
6
|
+
getAnnotationColor,
|
|
7
|
+
rectFromQuadPoints
|
|
8
|
+
} from "./utils.js";
|
|
9
|
+
function collectQuadSets(input) {
|
|
10
|
+
if (ArrayBuffer.isView(input)) {
|
|
11
|
+
const values = Array.from(input);
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (let index = 0; index + 7 < values.length; index += 8) {
|
|
14
|
+
chunks.push(values.slice(index, index + 8));
|
|
15
|
+
}
|
|
16
|
+
return chunks;
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(input)) return [];
|
|
19
|
+
if (input.length === 0) return [];
|
|
20
|
+
if (typeof input[0] === "number") {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
for (let index = 0; index + 7 < input.length; index += 8) {
|
|
23
|
+
chunks.push(input.slice(index, index + 8));
|
|
24
|
+
}
|
|
25
|
+
return chunks;
|
|
26
|
+
}
|
|
27
|
+
return input;
|
|
28
|
+
}
|
|
29
|
+
function toAnnotationType(pdfjsType) {
|
|
30
|
+
if (pdfjsType === PdfjsAnnotationType.HIGHLIGHT) return AnnotationType.HIGHLIGHT;
|
|
31
|
+
if (pdfjsType === PdfjsAnnotationType.UNDERLINE) return AnnotationType.UNDERLINE;
|
|
32
|
+
if (pdfjsType === PdfjsAnnotationType.STRIKEOUT) return AnnotationType.STRIKEOUT;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function createSegment(type, color, rect) {
|
|
36
|
+
if (type === AnnotationType.HIGHLIGHT) {
|
|
37
|
+
return new Konva.Rect({
|
|
38
|
+
x: rect.x,
|
|
39
|
+
y: rect.y,
|
|
40
|
+
width: rect.width,
|
|
41
|
+
height: rect.height,
|
|
42
|
+
opacity: 0.5,
|
|
43
|
+
fill: color
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (type === AnnotationType.UNDERLINE) {
|
|
47
|
+
return new Konva.Rect({
|
|
48
|
+
x: rect.x,
|
|
49
|
+
y: rect.y + rect.height - 1.5,
|
|
50
|
+
width: rect.width,
|
|
51
|
+
height: 0.5,
|
|
52
|
+
stroke: color,
|
|
53
|
+
strokeWidth: 0.5,
|
|
54
|
+
hitStrokeWidth: 10
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return new Konva.Rect({
|
|
58
|
+
x: rect.x,
|
|
59
|
+
y: rect.y + rect.height / 2,
|
|
60
|
+
width: rect.width,
|
|
61
|
+
height: 0.5,
|
|
62
|
+
stroke: color,
|
|
63
|
+
strokeWidth: 0.5,
|
|
64
|
+
hitStrokeWidth: 10
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function decodeHighlightAnnotation(annotation, context) {
|
|
68
|
+
if (typeof annotation.id !== "string" || typeof annotation.annotationType !== "number") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const type = toAnnotationType(annotation.annotationType);
|
|
72
|
+
if (type === null) return null;
|
|
73
|
+
const quadSets = collectQuadSets(annotation.quadPoints);
|
|
74
|
+
if (quadSets.length === 0) return null;
|
|
75
|
+
const color = getAnnotationColor(annotation, type);
|
|
76
|
+
const group = createGhostGroup(annotation.id);
|
|
77
|
+
for (const quadSet of quadSets) {
|
|
78
|
+
const rect = rectFromQuadPoints(quadSet, context.pageHeight);
|
|
79
|
+
if (!rect) continue;
|
|
80
|
+
group.add(createSegment(type, color, rect));
|
|
81
|
+
}
|
|
82
|
+
if (group.children.length === 0) {
|
|
83
|
+
group.destroy();
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const store = createAnnotationStore({
|
|
87
|
+
annotation,
|
|
88
|
+
allAnnotations: context.allAnnotations,
|
|
89
|
+
pageNumber: context.pageNumber,
|
|
90
|
+
type,
|
|
91
|
+
group,
|
|
92
|
+
color
|
|
93
|
+
});
|
|
94
|
+
group.destroy();
|
|
95
|
+
return store;
|
|
96
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import { AnnotationType } from "../engine/types.js";
|
|
3
|
+
import { isNumber } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
createAnnotationStore,
|
|
6
|
+
createGhostGroup,
|
|
7
|
+
getAnnotationColor,
|
|
8
|
+
normalizedStrokeWidth,
|
|
9
|
+
pointsFromInkList
|
|
10
|
+
} from "./utils.js";
|
|
11
|
+
export function decodeInkAnnotation(annotation, context) {
|
|
12
|
+
if (typeof annotation.id !== "string") return null;
|
|
13
|
+
if (!Array.isArray(annotation.inkLists) || annotation.inkLists.length === 0) return null;
|
|
14
|
+
const color = getAnnotationColor(annotation, AnnotationType.FREEHAND);
|
|
15
|
+
const strokeWidth = normalizedStrokeWidth(annotation, 1, { preferRawWidth: true });
|
|
16
|
+
const opacity = isNumber(annotation.opacity) ? annotation.opacity : 1;
|
|
17
|
+
const group = createGhostGroup(annotation.id);
|
|
18
|
+
for (const list of annotation.inkLists) {
|
|
19
|
+
const points = pointsFromInkList(list, context.pageHeight);
|
|
20
|
+
if (points.length < 4) continue;
|
|
21
|
+
group.add(new Konva.Line({
|
|
22
|
+
points,
|
|
23
|
+
strokeScaleEnabled: false,
|
|
24
|
+
stroke: color,
|
|
25
|
+
strokeWidth,
|
|
26
|
+
opacity,
|
|
27
|
+
lineCap: "round",
|
|
28
|
+
lineJoin: "round",
|
|
29
|
+
hitStrokeWidth: 20,
|
|
30
|
+
globalCompositeOperation: "source-over"
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
if (group.children.length === 0) {
|
|
34
|
+
group.destroy();
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const store = createAnnotationStore({
|
|
38
|
+
annotation,
|
|
39
|
+
allAnnotations: context.allAnnotations,
|
|
40
|
+
pageNumber: context.pageNumber,
|
|
41
|
+
type: AnnotationType.FREEHAND,
|
|
42
|
+
group,
|
|
43
|
+
color,
|
|
44
|
+
overrideSubtype: "Ink"
|
|
45
|
+
});
|
|
46
|
+
group.destroy();
|
|
47
|
+
return store;
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Konva from "konva";
|
|
2
|
+
import { AnnotationType } from "../engine/types.js";
|
|
3
|
+
import {
|
|
4
|
+
createAnnotationStore,
|
|
5
|
+
createGhostGroup,
|
|
6
|
+
dashedBorder,
|
|
7
|
+
getAnnotationColor,
|
|
8
|
+
hasArrowEnding,
|
|
9
|
+
linePointsFromCoordinates,
|
|
10
|
+
normalizedStrokeWidth
|
|
11
|
+
} from "./utils.js";
|
|
12
|
+
export function decodeLineAnnotation(annotation, context) {
|
|
13
|
+
if (typeof annotation.id !== "string") return null;
|
|
14
|
+
const points = linePointsFromCoordinates(annotation.lineCoordinates, context.pageHeight);
|
|
15
|
+
if (!points) return null;
|
|
16
|
+
const color = getAnnotationColor(annotation, AnnotationType.ARROW);
|
|
17
|
+
const strokeWidth = normalizedStrokeWidth(annotation, 1);
|
|
18
|
+
const lineEndings = Array.isArray(annotation.lineEndings) ? annotation.lineEndings : [];
|
|
19
|
+
const pointerAtBeginning = hasArrowEnding(lineEndings[0]);
|
|
20
|
+
const pointerAtEnding = hasArrowEnding(lineEndings[1]);
|
|
21
|
+
const group = createGhostGroup(annotation.id);
|
|
22
|
+
group.add(new Konva.Arrow({
|
|
23
|
+
points,
|
|
24
|
+
strokeScaleEnabled: false,
|
|
25
|
+
stroke: color,
|
|
26
|
+
fill: color,
|
|
27
|
+
strokeWidth,
|
|
28
|
+
hitStrokeWidth: 20,
|
|
29
|
+
lineCap: "round",
|
|
30
|
+
lineJoin: "round",
|
|
31
|
+
pointerLength: 10,
|
|
32
|
+
pointerWidth: 10,
|
|
33
|
+
pointerAtBeginning,
|
|
34
|
+
pointerAtEnding,
|
|
35
|
+
dash: dashedBorder(annotation)
|
|
36
|
+
}));
|
|
37
|
+
const store = createAnnotationStore({
|
|
38
|
+
annotation,
|
|
39
|
+
allAnnotations: context.allAnnotations,
|
|
40
|
+
pageNumber: context.pageNumber,
|
|
41
|
+
type: AnnotationType.ARROW,
|
|
42
|
+
group,
|
|
43
|
+
color,
|
|
44
|
+
overrideSubtype: "Arrow"
|
|
45
|
+
});
|
|
46
|
+
group.destroy();
|
|
47
|
+
return store;
|
|
48
|
+
}
|