kviewer 0.2.1 → 0.2.3
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 +19 -0
- package/dist/module.d.mts +8 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -3
- package/dist/runtime/annotation/engine/canvas-resolution.d.ts +7 -0
- package/dist/runtime/annotation/engine/canvas-resolution.js +21 -0
- package/dist/runtime/annotation/engine/editor/editor.d.ts +7 -2
- package/dist/runtime/annotation/engine/editor/editor.js +14 -0
- package/dist/runtime/annotation/engine/editor/selector.d.ts +13 -1
- package/dist/runtime/annotation/engine/editor/selector.js +60 -8
- package/dist/runtime/annotation/engine/import-normalize.js +3 -3
- package/dist/runtime/annotation/engine/painter.d.ts +33 -2
- package/dist/runtime/annotation/engine/painter.js +88 -2
- package/dist/runtime/annotation/engine/store.js +3 -0
- package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
- package/dist/runtime/annotation/engine/tools/circle.js +0 -2
- package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
- package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
- package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
- package/dist/runtime/annotation/engine/types.d.ts +9 -2
- package/dist/runtime/annotation/font-style.d.ts +15 -0
- package/dist/runtime/annotation/font-style.js +19 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
- package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
- package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
- package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
- package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
- package/dist/runtime/annotation/pdf-export/export.js +17 -4
- package/dist/runtime/annotation/pdf-export/parse.d.ts +22 -1
- package/dist/runtime/annotation/pdf-export/parse.js +32 -2
- package/dist/runtime/annotation/pdf-export/parse_circle.js +5 -5
- package/dist/runtime/annotation/pdf-export/parse_freetext.js +33 -24
- package/dist/runtime/annotation/pdf-export/parse_highlight.js +5 -5
- package/dist/runtime/annotation/pdf-export/parse_ink.js +7 -6
- package/dist/runtime/annotation/pdf-export/parse_line.js +7 -6
- package/dist/runtime/annotation/pdf-export/parse_polyline.js +7 -6
- package/dist/runtime/annotation/pdf-export/parse_square.js +5 -5
- package/dist/runtime/annotation/pdf-export/parse_stamp.js +6 -7
- package/dist/runtime/annotation/pdf-export/parse_strikeout.js +5 -5
- package/dist/runtime/annotation/pdf-export/parse_text.js +5 -5
- package/dist/runtime/annotation/pdf-export/parse_underline.js +5 -5
- package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
- package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
- package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
- package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
- package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
- package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
- package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
- package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
- package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
- package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
- package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
- package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/AnnotationToolbar.vue +6 -4
- package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
- package/dist/runtime/components/PdfPage.vue +170 -9
- package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
- package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
- package/dist/runtime/components/PerfHud.vue +97 -0
- package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
- package/dist/runtime/components/ToolButton.vue +4 -1
- package/dist/runtime/components/Viewer.d.vue.ts +25 -2
- package/dist/runtime/components/Viewer.vue +174 -12
- package/dist/runtime/components/Viewer.vue.d.ts +25 -2
- package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
- package/dist/runtime/components/ViewerBar.vue +183 -101
- package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
- package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
- package/dist/runtime/components/ViewerTabs.vue +13 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
- package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
- package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
- package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
- package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
- package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
- package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
- package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
- package/dist/runtime/components/panels/StampPicker.vue +3 -1
- package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
- package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
- package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
- package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
- package/dist/runtime/components/tools/PageSettings.vue +13 -11
- package/dist/runtime/components/tools/SearchTool.vue +12 -10
- package/dist/runtime/components/tools/ToolProperties.vue +7 -5
- package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
- package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
- package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
- package/dist/runtime/components/tools/ZoomControls.vue +17 -9
- package/dist/runtime/composables/canvas-limits.d.ts +22 -0
- package/dist/runtime/composables/canvas-limits.js +10 -0
- package/dist/runtime/composables/useAnnotationEngine.js +3 -1
- package/dist/runtime/composables/useFormFields.d.ts +6 -0
- package/dist/runtime/composables/useFormFields.js +28 -0
- package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
- package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
- package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
- package/dist/runtime/composables/useKviewerI18n.js +29 -0
- package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
- package/dist/runtime/composables/useScriptingManager.js +2 -0
- package/dist/runtime/composables/useViewerState.d.ts +4 -0
- package/dist/runtime/composables/useViewerState.js +8 -0
- package/dist/runtime/i18n/messages.d.ts +252 -0
- package/dist/runtime/i18n/messages.js +249 -0
- package/dist/runtime/public-types.d.ts +2 -0
- package/dist/runtime/toolbar-tools.d.ts +54 -0
- package/dist/runtime/toolbar-tools.js +79 -0
- package/dist/types.d.mts +1 -1
- package/package.json +14 -31
|
@@ -22,8 +22,6 @@ export class EditorFreeHighlight extends Editor {
|
|
|
22
22
|
this.getBgLayer().add(this.currentShapeGroup.konvaGroup);
|
|
23
23
|
const pos = this.konvaStage.getRelativePointerPosition();
|
|
24
24
|
this.line = new Konva.Line({
|
|
25
|
-
// do not scale strokes
|
|
26
|
-
strokeScaleEnabled: false,
|
|
27
25
|
stroke: this.currentAnnotation.style.color,
|
|
28
26
|
strokeWidth: this.currentAnnotation.style.strokeWidth,
|
|
29
27
|
opacity: this.currentAnnotation.style.opacity,
|
|
@@ -29,8 +29,6 @@ export class EditorFreeHand extends Editor {
|
|
|
29
29
|
}
|
|
30
30
|
const pos = this.konvaStage.getRelativePointerPosition();
|
|
31
31
|
this.line = new Konva.Line({
|
|
32
|
-
// do not scale strokes
|
|
33
|
-
strokeScaleEnabled: false,
|
|
34
32
|
stroke: this.currentAnnotation.style.color,
|
|
35
33
|
// Set the line color
|
|
36
34
|
strokeWidth: this.currentAnnotation.style.strokeWidth,
|
|
@@ -34,8 +34,6 @@ export class EditorRectangle extends Editor {
|
|
|
34
34
|
y: pos.y,
|
|
35
35
|
width: 0,
|
|
36
36
|
height: 0,
|
|
37
|
-
// do not scale strokes
|
|
38
|
-
strokeScaleEnabled: false,
|
|
39
37
|
visible: false,
|
|
40
38
|
stroke: this.currentAnnotation.style.color,
|
|
41
39
|
strokeWidth: this.currentAnnotation.style.strokeWidth || 2,
|
|
@@ -84,7 +84,8 @@ export interface IAnnotationStyle {
|
|
|
84
84
|
}
|
|
85
85
|
export interface IAnnotationComment {
|
|
86
86
|
id: string;
|
|
87
|
-
|
|
87
|
+
/** Author name. Undefined when the host app didn't supply a `userName`. */
|
|
88
|
+
title?: string;
|
|
88
89
|
date: string;
|
|
89
90
|
content: string;
|
|
90
91
|
status?: CommentStatus;
|
|
@@ -124,7 +125,8 @@ export interface IAnnotationStore {
|
|
|
124
125
|
pageNumber: number;
|
|
125
126
|
konvaString: string;
|
|
126
127
|
konvaClientRect: IRect;
|
|
127
|
-
|
|
128
|
+
/** Author name. Undefined when the host app didn't supply a `userName`. */
|
|
129
|
+
title?: string;
|
|
128
130
|
type: AnnotationType;
|
|
129
131
|
color?: string | null;
|
|
130
132
|
subtype: PdfjsAnnotationSubtype;
|
|
@@ -192,6 +194,11 @@ export interface FormFieldDefinition {
|
|
|
192
194
|
/** Signature-only: placeholder text shown on the unsigned widget.
|
|
193
195
|
* Defaults to 'Click to sign' when unset. */
|
|
194
196
|
promptText?: string;
|
|
197
|
+
/** Signature-only: the source PDF already carries a digital signature
|
|
198
|
+
* (`/V` is a `/Sig` dict) on this widget. The widget's appearance
|
|
199
|
+
* stream is rasterized after parse and seeded as the field value so
|
|
200
|
+
* the existing stamp is shown instead of the sign prompt. */
|
|
201
|
+
signedInSource?: boolean;
|
|
195
202
|
/** Signature-only: when this signature is filled, lock other fields in
|
|
196
203
|
* the form so they can no longer be edited. Mirrors PDF spec
|
|
197
204
|
* /Lock /Action: 'all' locks every other field; 'include' locks only
|
|
@@ -20,4 +20,19 @@ export interface DecodedFontStyle {
|
|
|
20
20
|
fontWeight: 'normal' | 'bold';
|
|
21
21
|
fontStyle: 'normal' | 'italic';
|
|
22
22
|
}
|
|
23
|
+
export declare const FONT_BORDER_SIZE = 2;
|
|
24
|
+
export declare const LINE_HEIGHT_FACTOR = 1.35;
|
|
25
|
+
export declare const DEFAULT_FIELD_FONT_SIZE = 9;
|
|
26
|
+
/**
|
|
27
|
+
* Compute the rendered font size (in PDF points) for a form field, matching
|
|
28
|
+
* pdf.js's `_setTextStyle` so text never gets clipped at the top/bottom of a
|
|
29
|
+
* short field. A declared size of 0 / undefined means the widget's `/DA` had
|
|
30
|
+
* no explicit size ("auto") — we fall back to 9pt before fitting, exactly
|
|
31
|
+
* like pdf.js. Single-line fields shrink the font to fit the field height;
|
|
32
|
+
* multi-line fields fit N lines of the declared size and shrink only if the
|
|
33
|
+
* per-line height is smaller than declared.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeFieldFontSize(declaredSize: number | undefined, rectHeightPts: number, opts?: {
|
|
36
|
+
multiLine?: boolean;
|
|
37
|
+
}): number;
|
|
23
38
|
export declare function decodeFontName(fontName: string | undefined): DecodedFontStyle;
|
|
@@ -32,6 +32,25 @@ const DEFAULT_STYLE = {
|
|
|
32
32
|
fontWeight: "normal",
|
|
33
33
|
fontStyle: "normal"
|
|
34
34
|
};
|
|
35
|
+
export const FONT_BORDER_SIZE = 2;
|
|
36
|
+
export const LINE_HEIGHT_FACTOR = 1.35;
|
|
37
|
+
export const DEFAULT_FIELD_FONT_SIZE = 9;
|
|
38
|
+
export function computeFieldFontSize(declaredSize, rectHeightPts, opts) {
|
|
39
|
+
const declared = declaredSize && declaredSize > 0 ? declaredSize : DEFAULT_FIELD_FONT_SIZE;
|
|
40
|
+
const rectHeight = Math.abs(rectHeightPts - FONT_BORDER_SIZE);
|
|
41
|
+
let size;
|
|
42
|
+
if (opts?.multiLine) {
|
|
43
|
+
const numberOfLines = Math.max(
|
|
44
|
+
1,
|
|
45
|
+
Math.round(rectHeight / (LINE_HEIGHT_FACTOR * declared))
|
|
46
|
+
);
|
|
47
|
+
const lineHeight = rectHeight / numberOfLines;
|
|
48
|
+
size = Math.min(declared, lineHeight / LINE_HEIGHT_FACTOR);
|
|
49
|
+
} else {
|
|
50
|
+
size = Math.min(declared, rectHeight / LINE_HEIGHT_FACTOR);
|
|
51
|
+
}
|
|
52
|
+
return Math.round(10 * size) / 10;
|
|
53
|
+
}
|
|
35
54
|
export function decodeFontName(fontName) {
|
|
36
55
|
if (!fontName) return DEFAULT_STYLE;
|
|
37
56
|
const name = fontName.startsWith("/") ? fontName.slice(1) : fontName;
|
|
@@ -10,6 +10,14 @@ import type { CheckboxStyle } from '../engine/types.js';
|
|
|
10
10
|
* wins.
|
|
11
11
|
*/
|
|
12
12
|
export declare function extractCheckboxStyles(pdfBytes: Uint8Array | ArrayBuffer): Promise<Map<string, CheckboxStyle>>;
|
|
13
|
+
/**
|
|
14
|
+
* Walk the AcroForm and collect the fully qualified names of signature
|
|
15
|
+
* fields that are already digitally signed (`/V` holds a `/Sig` dict).
|
|
16
|
+
* pdf.js deliberately nulls a signature widget's fieldValue before
|
|
17
|
+
* shipping it to the main thread (the `/Sig` dict is non-serializable),
|
|
18
|
+
* so the signed state is only reachable through this side-channel parse.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractSignedSignatureNames(pdfBytes: Uint8Array | ArrayBuffer): Promise<Set<string>>;
|
|
13
21
|
/**
|
|
14
22
|
* Walk the AcroForm and extract push-button captions from `/MK /CA`.
|
|
15
23
|
* pdf.js exposes neither the caption nor a button label, so without this
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PDFButton, PDFCheckBox, PDFDocument, PDFDict, PDFHexString, PDFName, PDFString } from "pdf-lib";
|
|
1
|
+
import { PDFButton, PDFCheckBox, PDFDocument, PDFDict, PDFHexString, PDFName, PDFSignature, PDFString } from "pdf-lib";
|
|
2
2
|
import { captionToStyle } from "../checkbox-styles.js";
|
|
3
3
|
export async function extractCheckboxStyles(pdfBytes) {
|
|
4
4
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -38,6 +38,29 @@ export async function extractCheckboxStyles(pdfBytes) {
|
|
|
38
38
|
}
|
|
39
39
|
return out;
|
|
40
40
|
}
|
|
41
|
+
export async function extractSignedSignatureNames(pdfBytes) {
|
|
42
|
+
const out = /* @__PURE__ */ new Set();
|
|
43
|
+
let doc;
|
|
44
|
+
try {
|
|
45
|
+
doc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
let form;
|
|
50
|
+
try {
|
|
51
|
+
form = doc.getForm();
|
|
52
|
+
} catch {
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
for (const field of form.getFields()) {
|
|
56
|
+
if (!(field instanceof PDFSignature)) continue;
|
|
57
|
+
const name = field.getName();
|
|
58
|
+
if (!name) continue;
|
|
59
|
+
const value = field.acroField.getInheritableAttribute(PDFName.of("V"));
|
|
60
|
+
if (value != null) out.add(name);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
41
64
|
export async function extractButtonCaptions(pdfBytes) {
|
|
42
65
|
const out = /* @__PURE__ */ new Map();
|
|
43
66
|
let doc;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PDFNumber } from 'pdf-lib';
|
|
2
|
+
/** /F entry for exported annotations: visible AND printed (PDF 32000-1 §12.5.3).
|
|
3
|
+
* Annotations without the Print bit are silently dropped from print output,
|
|
4
|
+
* so every exporter must set this on each annotation dict it creates. */
|
|
5
|
+
export declare const PRINT_FLAG: PDFNumber;
|
|
6
|
+
/** Print + Locked — stamps stay printable but can't be moved/deleted in
|
|
7
|
+
* other viewers, matching the previous Locked-only behavior. */
|
|
8
|
+
export declare const PRINT_LOCKED_FLAG: PDFNumber;
|
|
@@ -272,7 +272,9 @@ async function createSignatureField(pdfDoc, page, def, fv) {
|
|
|
272
272
|
Rect: [x1, y1, x2, y2],
|
|
273
273
|
P: page.ref,
|
|
274
274
|
Parent: fieldRef,
|
|
275
|
-
|
|
275
|
+
// AnnotationFlags.Print is already the bit value (4) — shifting it
|
|
276
|
+
// again would yield 16 = NoZoom.
|
|
277
|
+
F: AnnotationFlags.Print,
|
|
276
278
|
AP: { N: appearanceRef }
|
|
277
279
|
});
|
|
278
280
|
const widgetRef = context.register(widgetDict);
|
|
@@ -2,6 +2,7 @@ import { degrees, PDFArray, PDFDict, PDFDocument, PDFName, PDFRef, PDFString } f
|
|
|
2
2
|
import { PdfjsAnnotationType } from "../engine/types.js";
|
|
3
3
|
import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
|
|
4
4
|
import { writeFormFieldsToPdf } from "./export-form-fields.js";
|
|
5
|
+
import { getUnicodeFont } from "./unicode-font.js";
|
|
5
6
|
import { CircleParser } from "./parse_circle.js";
|
|
6
7
|
import { FreeTextParser } from "./parse_freetext.js";
|
|
7
8
|
import { HighlightParser } from "./parse_highlight.js";
|
|
@@ -124,13 +125,13 @@ function removeOwnedAnnotations(pdfDoc, ownedIds, deletedIds) {
|
|
|
124
125
|
page.node.set(annotsKey, context.obj(kept));
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
|
-
async function parseAnnotationToPdf(annotation, page, pdfDoc) {
|
|
128
|
+
async function parseAnnotationToPdf(annotation, page, pdfDoc, isModifiedOriginal) {
|
|
128
129
|
const ParserClass = parserMap[annotation.pdfjsType];
|
|
129
130
|
if (!ParserClass) {
|
|
130
131
|
console.warn(`Unsupported annotation type for PDF export: ${annotation.pdfjsType}`);
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
133
|
-
const parser = new ParserClass(pdfDoc, page, annotation);
|
|
134
|
+
const parser = new ParserClass(pdfDoc, page, annotation, isModifiedOriginal);
|
|
134
135
|
await parser.parse();
|
|
135
136
|
}
|
|
136
137
|
async function embedDataUrlImage(pdfDoc, dataUrl) {
|
|
@@ -296,8 +297,20 @@ export async function exportAnnotationsToPdf({
|
|
|
296
297
|
console.warn(`Skipping annotation ${annotation.id}; page ${annotation.pageNumber} missing`);
|
|
297
298
|
continue;
|
|
298
299
|
}
|
|
299
|
-
|
|
300
|
+
const isModifiedOriginal = originalAnnotationIds?.has(annotation.id) ?? false;
|
|
301
|
+
await parseAnnotationToPdf(annotation, page, pdfDoc, isModifiedOriginal);
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
|
-
return pdfDoc
|
|
304
|
+
return savePdf(pdfDoc);
|
|
305
|
+
}
|
|
306
|
+
async function savePdf(pdfDoc) {
|
|
307
|
+
try {
|
|
308
|
+
return await pdfDoc.save();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
const message = err instanceof Error ? err.message : "";
|
|
311
|
+
if (!/cannot encode/i.test(message)) throw err;
|
|
312
|
+
const font = await getUnicodeFont(pdfDoc);
|
|
313
|
+
pdfDoc.getForm().updateFieldAppearances(font);
|
|
314
|
+
return pdfDoc.save({ updateFieldAppearances: false });
|
|
315
|
+
}
|
|
303
316
|
}
|
|
@@ -4,7 +4,28 @@ export declare abstract class AnnotationParser {
|
|
|
4
4
|
protected annotation: IAnnotationStore;
|
|
5
5
|
protected page: PDFPage;
|
|
6
6
|
protected pdfDoc: PDFDocument;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Whether this annotation already existed in the source PDF and is being
|
|
9
|
+
* rewritten because the user edited it. Drives the date key, mirroring
|
|
10
|
+
* pdf.js: `CreationDate` for newly authored annotations, `M` (modification
|
|
11
|
+
* date) for edits to pre-existing ones.
|
|
12
|
+
*/
|
|
13
|
+
protected isModifiedOriginal: boolean;
|
|
14
|
+
constructor(pdfDoc: PDFDocument, page: PDFPage, annotation: IAnnotationStore, isModifiedOriginal?: boolean);
|
|
15
|
+
/**
|
|
16
|
+
* Owner (`/T`) and date entries shared by every markup annotation and its
|
|
17
|
+
* reply comments. We align with pdf.js / Firefox here:
|
|
18
|
+
*
|
|
19
|
+
* - `/T` (author) is written ONLY when a real author name is present. When
|
|
20
|
+
* the host app doesn't supply a `userName`, no `/T` is emitted — so
|
|
21
|
+
* archiving systems have no author to burn into the rendered preview.
|
|
22
|
+
* (kviewer previously always wrote `/T`, defaulting to "User".)
|
|
23
|
+
* - The date goes to `/CreationDate` for newly authored annotations and to
|
|
24
|
+
* `/M` when an existing annotation was edited.
|
|
25
|
+
*
|
|
26
|
+
* Spread the result into the annotation's dict literal before `context.obj`.
|
|
27
|
+
*/
|
|
28
|
+
protected ownerDateEntries(title: string | undefined, date: string | undefined): Record<string, unknown>;
|
|
8
29
|
protected addAnnotationToPage(page: PDFPage, annotRef: PDFRef): void;
|
|
9
30
|
abstract parse(): Promise<void>;
|
|
10
31
|
}
|
|
@@ -1,12 +1,42 @@
|
|
|
1
|
-
import { PDFName } from "pdf-lib";
|
|
1
|
+
import { PDFName, PDFString } from "pdf-lib";
|
|
2
|
+
import { stringToPDFHexString } from "../engine/utils.js";
|
|
2
3
|
export class AnnotationParser {
|
|
3
4
|
annotation;
|
|
4
5
|
page;
|
|
5
6
|
pdfDoc;
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Whether this annotation already existed in the source PDF and is being
|
|
9
|
+
* rewritten because the user edited it. Drives the date key, mirroring
|
|
10
|
+
* pdf.js: `CreationDate` for newly authored annotations, `M` (modification
|
|
11
|
+
* date) for edits to pre-existing ones.
|
|
12
|
+
*/
|
|
13
|
+
isModifiedOriginal;
|
|
14
|
+
constructor(pdfDoc, page, annotation, isModifiedOriginal = false) {
|
|
7
15
|
this.pdfDoc = pdfDoc;
|
|
8
16
|
this.page = page;
|
|
9
17
|
this.annotation = annotation;
|
|
18
|
+
this.isModifiedOriginal = isModifiedOriginal;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Owner (`/T`) and date entries shared by every markup annotation and its
|
|
22
|
+
* reply comments. We align with pdf.js / Firefox here:
|
|
23
|
+
*
|
|
24
|
+
* - `/T` (author) is written ONLY when a real author name is present. When
|
|
25
|
+
* the host app doesn't supply a `userName`, no `/T` is emitted — so
|
|
26
|
+
* archiving systems have no author to burn into the rendered preview.
|
|
27
|
+
* (kviewer previously always wrote `/T`, defaulting to "User".)
|
|
28
|
+
* - The date goes to `/CreationDate` for newly authored annotations and to
|
|
29
|
+
* `/M` when an existing annotation was edited.
|
|
30
|
+
*
|
|
31
|
+
* Spread the result into the annotation's dict literal before `context.obj`.
|
|
32
|
+
*/
|
|
33
|
+
ownerDateEntries(title, date) {
|
|
34
|
+
const entries = {};
|
|
35
|
+
if (title) entries.T = stringToPDFHexString(title);
|
|
36
|
+
if (date) {
|
|
37
|
+
entries[this.isModifiedOriginal ? "M" : "CreationDate"] = PDFString.of(date);
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
10
40
|
}
|
|
11
41
|
addAnnotationToPage(page, annotRef) {
|
|
12
42
|
const annots = page.node.lookup(PDFName.of("Annots"));
|
|
@@ -3,7 +3,7 @@ import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "
|
|
|
3
3
|
import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
|
|
4
4
|
import { AnnotationParser } from "./parse.js";
|
|
5
5
|
import { computeBoundsFromKonvaString } from "./parse_utils.js";
|
|
6
|
-
|
|
6
|
+
import { PRINT_FLAG } from "./annotation-flags.js";
|
|
7
7
|
export class CircleParser extends AnnotationParser {
|
|
8
8
|
async parse() {
|
|
9
9
|
const { annotation, page, pdfDoc } = this;
|
|
@@ -14,11 +14,11 @@ export class CircleParser extends AnnotationParser {
|
|
|
14
14
|
const mainAnn = context.obj({
|
|
15
15
|
Type: PDFName.of("Annot"),
|
|
16
16
|
Subtype: PDFName.of("Circle"),
|
|
17
|
+
F: PRINT_FLAG,
|
|
17
18
|
Rect: rect,
|
|
18
19
|
C: rgbToPdfColor(annotation.color || void 0),
|
|
19
|
-
|
|
20
|
+
...this.ownerDateEntries(annotation.title, annotation.date),
|
|
20
21
|
Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
|
|
21
|
-
M: PDFString.of(annotation.date || ""),
|
|
22
22
|
NM: PDFString.of(annotation.id),
|
|
23
23
|
Border: [0, 0, 1]
|
|
24
24
|
});
|
|
@@ -28,10 +28,10 @@ export class CircleParser extends AnnotationParser {
|
|
|
28
28
|
const replyAnn = context.obj({
|
|
29
29
|
Type: PDFName.of("Annot"),
|
|
30
30
|
Subtype: PDFName.of("Text"),
|
|
31
|
+
F: PRINT_FLAG,
|
|
31
32
|
Rect: rect,
|
|
32
33
|
Contents: stringToPDFHexString(comment.content),
|
|
33
|
-
|
|
34
|
-
M: PDFString.of(comment.date || ""),
|
|
34
|
+
...this.ownerDateEntries(comment.title, comment.date),
|
|
35
35
|
C: rgbToPdfColor(annotation.color || void 0),
|
|
36
36
|
IRT: mainAnnRef,
|
|
37
37
|
RT: PDFName.of("R"),
|
|
@@ -3,7 +3,9 @@ import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "
|
|
|
3
3
|
import { convertCanvasRectToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
|
|
4
4
|
import { AnnotationParser } from "./parse.js";
|
|
5
5
|
import { formAppearanceMatrix } from "./parse_utils.js";
|
|
6
|
-
|
|
6
|
+
import { encodeWinAnsi, isWinAnsiEncodable } from "./winansi.js";
|
|
7
|
+
import { getUnicodeFont } from "./unicode-font.js";
|
|
8
|
+
import { PRINT_FLAG } from "./annotation-flags.js";
|
|
7
9
|
function formatPdfNumber(value) {
|
|
8
10
|
if (!Number.isFinite(value)) return "0";
|
|
9
11
|
return Number(value.toFixed(3)).toString();
|
|
@@ -63,41 +65,50 @@ export class FreeTextParser extends AnnotationParser {
|
|
|
63
65
|
];
|
|
64
66
|
const appearanceWidth = Math.max(1, textWidth);
|
|
65
67
|
const appearanceHeight = Math.max(1, textHeight);
|
|
68
|
+
const useUnicodeFont = !isWinAnsiEncodable(text);
|
|
69
|
+
let unicodeFont = null;
|
|
70
|
+
if (useUnicodeFont) {
|
|
71
|
+
unicodeFont = await getUnicodeFont(pdfDoc);
|
|
72
|
+
}
|
|
73
|
+
const fontResourceName = useUnicodeFont ? "FUni" : "Helvetica";
|
|
66
74
|
const commands = ["q"];
|
|
67
75
|
for (let i = 0; i < lines.length; i++) {
|
|
68
76
|
const line = lines[i] ?? "";
|
|
69
77
|
if (line.length === 0 && i < lines.length - 1) continue;
|
|
70
78
|
const lineY = appearanceHeight - (i + 0.8) * lineHeight;
|
|
71
79
|
let lineX = 0;
|
|
72
|
-
if (quadding === 1) {
|
|
73
|
-
const lineWidth = line.length * estimatedCharWidth;
|
|
74
|
-
lineX = Math.max(0, (appearanceWidth - lineWidth) / 2);
|
|
75
|
-
} else if (quadding === 2) {
|
|
76
|
-
const lineWidth = line.length * estimatedCharWidth;
|
|
77
|
-
lineX = Math.max(0, appearanceWidth - lineWidth);
|
|
80
|
+
if (quadding === 1 || quadding === 2) {
|
|
81
|
+
const lineWidth = unicodeFont ? unicodeFont.widthOfTextAtSize(line, fontSize) : line.length * estimatedCharWidth;
|
|
82
|
+
lineX = quadding === 1 ? Math.max(0, (appearanceWidth - lineWidth) / 2) : Math.max(0, appearanceWidth - lineWidth);
|
|
78
83
|
}
|
|
84
|
+
const showText = unicodeFont ? `${unicodeFont.encodeText(line).toString()} Tj` : `(${escapePdfString(line)}) Tj`;
|
|
79
85
|
commands.push(
|
|
80
86
|
"BT",
|
|
81
|
-
|
|
87
|
+
`/${fontResourceName} ${formatPdfNumber(fontSize)} Tf`,
|
|
82
88
|
`${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`,
|
|
83
89
|
`${formatPdfNumber(lineX)} ${formatPdfNumber(lineY)} Td`,
|
|
84
|
-
|
|
90
|
+
showText,
|
|
85
91
|
"ET"
|
|
86
92
|
);
|
|
87
93
|
}
|
|
88
94
|
commands.push("Q");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
let fontResourceRef;
|
|
96
|
+
if (unicodeFont) {
|
|
97
|
+
fontResourceRef = unicodeFont.ref;
|
|
98
|
+
} else {
|
|
99
|
+
fontResourceRef = context.register(context.obj({
|
|
100
|
+
Type: PDFName.of("Font"),
|
|
101
|
+
Subtype: PDFName.of("Type1"),
|
|
102
|
+
BaseFont: PDFName.of("Helvetica"),
|
|
103
|
+
Encoding: PDFName.of("WinAnsiEncoding")
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
96
106
|
const appearanceMatrix = formAppearanceMatrix(
|
|
97
107
|
pageTransform.rotation,
|
|
98
108
|
appearanceWidth,
|
|
99
109
|
appearanceHeight
|
|
100
110
|
);
|
|
111
|
+
const streamBytes = unicodeFont ? new TextEncoder().encode(commands.join("\n")) : encodeWinAnsi(commands.join("\n"));
|
|
101
112
|
const appearanceStream = PDFRawStream.of(
|
|
102
113
|
context.obj({
|
|
103
114
|
Type: PDFName.of("XObject"),
|
|
@@ -105,10 +116,10 @@ export class FreeTextParser extends AnnotationParser {
|
|
|
105
116
|
BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
|
|
106
117
|
Matrix: context.obj(appearanceMatrix.map((n) => PDFNumber.of(n))),
|
|
107
118
|
Resources: context.obj({
|
|
108
|
-
Font: context.obj({
|
|
119
|
+
Font: context.obj({ [fontResourceName]: fontResourceRef })
|
|
109
120
|
})
|
|
110
121
|
}),
|
|
111
|
-
|
|
122
|
+
streamBytes
|
|
112
123
|
);
|
|
113
124
|
const appearanceRef = context.register(appearanceStream);
|
|
114
125
|
const ap = context.obj({ N: appearanceRef });
|
|
@@ -116,17 +127,15 @@ export class FreeTextParser extends AnnotationParser {
|
|
|
116
127
|
const mainAnn = context.obj({
|
|
117
128
|
Type: PDFName.of("Annot"),
|
|
118
129
|
Subtype: PDFName.of("FreeText"),
|
|
130
|
+
F: PRINT_FLAG,
|
|
119
131
|
Rect: rect,
|
|
120
132
|
NM: PDFString.of(annotation.id),
|
|
121
133
|
Contents: stringToPDFHexString(text),
|
|
122
134
|
DA: PDFString.of(da),
|
|
123
135
|
Q: PDFNumber.of(quadding),
|
|
124
|
-
|
|
125
|
-
M: PDFString.of(annotation.date || ""),
|
|
136
|
+
...this.ownerDateEntries(annotation.title, annotation.date),
|
|
126
137
|
C: context.obj(rgbToPdfColor(annotation.color || void 0)),
|
|
127
138
|
Border: context.obj([0, 0, 0]),
|
|
128
|
-
F: PDFNumber.of(4),
|
|
129
|
-
// Print flag
|
|
130
139
|
AP: ap
|
|
131
140
|
});
|
|
132
141
|
const mainAnnRef = context.register(mainAnn);
|
|
@@ -135,10 +144,10 @@ export class FreeTextParser extends AnnotationParser {
|
|
|
135
144
|
const replyAnn = context.obj({
|
|
136
145
|
Type: PDFName.of("Annot"),
|
|
137
146
|
Subtype: PDFName.of("Text"),
|
|
147
|
+
F: PRINT_FLAG,
|
|
138
148
|
Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
|
|
139
149
|
Contents: stringToPDFHexString(comment.content),
|
|
140
|
-
|
|
141
|
-
M: PDFString.of(comment.date || ""),
|
|
150
|
+
...this.ownerDateEntries(comment.title, comment.date),
|
|
142
151
|
C: context.obj(rgbToPdfColor(annotation.color || void 0)),
|
|
143
152
|
IRT: mainAnnRef,
|
|
144
153
|
RT: PDFName.of("R"),
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
pageTransformFromPdfLibPage
|
|
7
7
|
} from "../pdf-import/transform.js";
|
|
8
8
|
import { AnnotationParser } from "./parse.js";
|
|
9
|
-
|
|
9
|
+
import { PRINT_FLAG } from "./annotation-flags.js";
|
|
10
10
|
function formatPdfNumber(value) {
|
|
11
11
|
if (!Number.isFinite(value)) return "0";
|
|
12
12
|
return Number(value.toFixed(3)).toString();
|
|
@@ -115,12 +115,12 @@ export class HighlightParser extends AnnotationParser {
|
|
|
115
115
|
const mainAnn = context.obj({
|
|
116
116
|
Type: PDFName.of("Annot"),
|
|
117
117
|
Subtype: PDFName.of("Highlight"),
|
|
118
|
+
F: PRINT_FLAG,
|
|
118
119
|
Rect: rect,
|
|
119
120
|
QuadPoints: context.obj(quadPoints),
|
|
120
121
|
C: context.obj(color),
|
|
121
|
-
|
|
122
|
+
...this.ownerDateEntries(annotation.title, annotation.date),
|
|
122
123
|
Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
|
|
123
|
-
M: PDFString.of(annotation.date || ""),
|
|
124
124
|
NM: PDFString.of(annotation.id),
|
|
125
125
|
CA: PDFNumber.of(0.5),
|
|
126
126
|
AP: ap
|
|
@@ -131,10 +131,10 @@ export class HighlightParser extends AnnotationParser {
|
|
|
131
131
|
const replyAnn = context.obj({
|
|
132
132
|
Type: PDFName.of("Annot"),
|
|
133
133
|
Subtype: PDFName.of("Text"),
|
|
134
|
+
F: PRINT_FLAG,
|
|
134
135
|
Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
|
|
135
136
|
Contents: stringToPDFHexString(comment.content),
|
|
136
|
-
|
|
137
|
-
M: PDFString.of(comment.date || ""),
|
|
137
|
+
...this.ownerDateEntries(comment.title, comment.date),
|
|
138
138
|
C: rgbToPdfColor(annotation.color || void 0),
|
|
139
139
|
IRT: mainAnnRef,
|
|
140
140
|
RT: PDFName.of("R"),
|
|
@@ -2,7 +2,7 @@ import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
|
|
|
2
2
|
import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
|
|
3
3
|
import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
|
|
4
4
|
import { AnnotationParser } from "./parse.js";
|
|
5
|
-
|
|
5
|
+
import { PRINT_FLAG } from "./annotation-flags.js";
|
|
6
6
|
function formatPdfNumber(value) {
|
|
7
7
|
if (!Number.isFinite(value)) return "0";
|
|
8
8
|
return Number(value.toFixed(3)).toString();
|
|
@@ -45,7 +45,8 @@ export class InkParser extends AnnotationParser {
|
|
|
45
45
|
transformedPaths.map((path) => context.obj(path))
|
|
46
46
|
);
|
|
47
47
|
const firstLine = lines[0]?.attrs || {};
|
|
48
|
-
const
|
|
48
|
+
const strokeScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
|
|
49
|
+
const strokeWidth = (firstLine.strokeWidth ?? 1) * strokeScale;
|
|
49
50
|
const opacity = firstLine.opacity ?? 1;
|
|
50
51
|
const color = firstLine.stroke ?? annotation.color ?? "rgb(255, 0, 0)";
|
|
51
52
|
const [r, g, b] = rgbToPdfColor(color);
|
|
@@ -120,12 +121,12 @@ export class InkParser extends AnnotationParser {
|
|
|
120
121
|
const mainAnn = context.obj({
|
|
121
122
|
Type: PDFName.of("Annot"),
|
|
122
123
|
Subtype: PDFName.of("Ink"),
|
|
124
|
+
F: PRINT_FLAG,
|
|
123
125
|
Rect: rect,
|
|
124
126
|
InkList: inkList,
|
|
125
127
|
C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
|
|
126
|
-
|
|
128
|
+
...this.ownerDateEntries(annotation.title, annotation.date),
|
|
127
129
|
Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
|
|
128
|
-
M: PDFString.of(annotation.date || ""),
|
|
129
130
|
NM: PDFString.of(annotation.id),
|
|
130
131
|
Border: context.obj([0, 0, 0]),
|
|
131
132
|
BS: bs,
|
|
@@ -138,10 +139,10 @@ export class InkParser extends AnnotationParser {
|
|
|
138
139
|
const replyAnn = context.obj({
|
|
139
140
|
Type: PDFName.of("Annot"),
|
|
140
141
|
Subtype: PDFName.of("Text"),
|
|
142
|
+
F: PRINT_FLAG,
|
|
141
143
|
Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
|
|
142
144
|
Contents: stringToPDFHexString(comment.content),
|
|
143
|
-
|
|
144
|
-
M: PDFString.of(comment.date || ""),
|
|
145
|
+
...this.ownerDateEntries(comment.title, comment.date),
|
|
145
146
|
C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
|
|
146
147
|
IRT: mainAnnRef,
|
|
147
148
|
RT: PDFName.of("R"),
|
|
@@ -2,7 +2,7 @@ import { PDFName, PDFNumber, PDFRawStream, PDFString } from "pdf-lib";
|
|
|
2
2
|
import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "../engine/utils.js";
|
|
3
3
|
import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
|
|
4
4
|
import { AnnotationParser } from "./parse.js";
|
|
5
|
-
|
|
5
|
+
import { PRINT_FLAG } from "./annotation-flags.js";
|
|
6
6
|
function formatPdfNumber(value) {
|
|
7
7
|
if (!Number.isFinite(value)) return "0";
|
|
8
8
|
return Number(value.toFixed(3)).toString();
|
|
@@ -39,7 +39,8 @@ export class LineParser extends AnnotationParser {
|
|
|
39
39
|
transformedPaths.map((path) => context.obj(path))
|
|
40
40
|
);
|
|
41
41
|
const firstLine = lines[0]?.attrs || {};
|
|
42
|
-
const
|
|
42
|
+
const strokeScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
|
|
43
|
+
const strokeWidth = (firstLine.strokeWidth ?? 1) * strokeScale;
|
|
43
44
|
const opacity = firstLine.opacity ?? 1;
|
|
44
45
|
const color = firstLine.stroke ?? annotation.color ?? "rgb(255, 0, 0)";
|
|
45
46
|
const [r, g, b] = rgbToPdfColor(color);
|
|
@@ -158,12 +159,12 @@ export class LineParser extends AnnotationParser {
|
|
|
158
159
|
const mainAnn = context.obj({
|
|
159
160
|
Type: PDFName.of("Annot"),
|
|
160
161
|
Subtype: PDFName.of("Ink"),
|
|
162
|
+
F: PRINT_FLAG,
|
|
161
163
|
Rect: rect,
|
|
162
164
|
InkList: inkList,
|
|
163
165
|
C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
|
|
164
|
-
|
|
166
|
+
...this.ownerDateEntries(annotation.title, annotation.date),
|
|
165
167
|
Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
|
|
166
|
-
M: PDFString.of(annotation.date || ""),
|
|
167
168
|
NM: PDFString.of(annotation.id),
|
|
168
169
|
Border: context.obj([0, 0, 0]),
|
|
169
170
|
BS: bs,
|
|
@@ -176,10 +177,10 @@ export class LineParser extends AnnotationParser {
|
|
|
176
177
|
const replyAnn = context.obj({
|
|
177
178
|
Type: PDFName.of("Annot"),
|
|
178
179
|
Subtype: PDFName.of("Text"),
|
|
180
|
+
F: PRINT_FLAG,
|
|
179
181
|
Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
|
|
180
182
|
Contents: stringToPDFHexString(comment.content),
|
|
181
|
-
|
|
182
|
-
M: PDFString.of(comment.date || ""),
|
|
183
|
+
...this.ownerDateEntries(comment.title, comment.date),
|
|
183
184
|
C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
|
|
184
185
|
IRT: mainAnnRef,
|
|
185
186
|
RT: PDFName.of("R"),
|