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.
Files changed (111) hide show
  1. package/README.md +19 -0
  2. package/dist/module.d.mts +8 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +25 -3
  5. package/dist/runtime/annotation/engine/canvas-resolution.d.ts +7 -0
  6. package/dist/runtime/annotation/engine/canvas-resolution.js +21 -0
  7. package/dist/runtime/annotation/engine/editor/editor.d.ts +7 -2
  8. package/dist/runtime/annotation/engine/editor/editor.js +14 -0
  9. package/dist/runtime/annotation/engine/editor/selector.d.ts +13 -1
  10. package/dist/runtime/annotation/engine/editor/selector.js +60 -8
  11. package/dist/runtime/annotation/engine/import-normalize.js +3 -3
  12. package/dist/runtime/annotation/engine/painter.d.ts +33 -2
  13. package/dist/runtime/annotation/engine/painter.js +88 -2
  14. package/dist/runtime/annotation/engine/store.js +3 -0
  15. package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
  16. package/dist/runtime/annotation/engine/tools/circle.js +0 -2
  17. package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
  18. package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
  19. package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
  20. package/dist/runtime/annotation/engine/types.d.ts +9 -2
  21. package/dist/runtime/annotation/font-style.d.ts +15 -0
  22. package/dist/runtime/annotation/font-style.js +19 -0
  23. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
  24. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
  25. package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
  26. package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
  27. package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
  28. package/dist/runtime/annotation/pdf-export/export.js +17 -4
  29. package/dist/runtime/annotation/pdf-export/parse.d.ts +22 -1
  30. package/dist/runtime/annotation/pdf-export/parse.js +32 -2
  31. package/dist/runtime/annotation/pdf-export/parse_circle.js +5 -5
  32. package/dist/runtime/annotation/pdf-export/parse_freetext.js +33 -24
  33. package/dist/runtime/annotation/pdf-export/parse_highlight.js +5 -5
  34. package/dist/runtime/annotation/pdf-export/parse_ink.js +7 -6
  35. package/dist/runtime/annotation/pdf-export/parse_line.js +7 -6
  36. package/dist/runtime/annotation/pdf-export/parse_polyline.js +7 -6
  37. package/dist/runtime/annotation/pdf-export/parse_square.js +5 -5
  38. package/dist/runtime/annotation/pdf-export/parse_stamp.js +6 -7
  39. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +5 -5
  40. package/dist/runtime/annotation/pdf-export/parse_text.js +5 -5
  41. package/dist/runtime/annotation/pdf-export/parse_underline.js +5 -5
  42. package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
  43. package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
  44. package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
  45. package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
  46. package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
  47. package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
  48. package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
  49. package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
  50. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
  51. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
  52. package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
  53. package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
  54. package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
  55. package/dist/runtime/assets/kviewer.css +1 -1
  56. package/dist/runtime/components/AnnotationToolbar.vue +6 -4
  57. package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
  58. package/dist/runtime/components/PdfPage.vue +170 -9
  59. package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
  60. package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
  61. package/dist/runtime/components/PerfHud.vue +97 -0
  62. package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
  63. package/dist/runtime/components/ToolButton.vue +4 -1
  64. package/dist/runtime/components/Viewer.d.vue.ts +25 -2
  65. package/dist/runtime/components/Viewer.vue +174 -12
  66. package/dist/runtime/components/Viewer.vue.d.ts +25 -2
  67. package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
  68. package/dist/runtime/components/ViewerBar.vue +183 -101
  69. package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
  70. package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
  71. package/dist/runtime/components/ViewerTabs.vue +13 -2
  72. package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
  73. package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
  74. package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
  75. package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
  76. package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
  77. package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
  78. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
  79. package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
  80. package/dist/runtime/components/panels/StampPicker.vue +3 -1
  81. package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
  82. package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
  83. package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
  84. package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
  85. package/dist/runtime/components/tools/PageSettings.vue +13 -11
  86. package/dist/runtime/components/tools/SearchTool.vue +12 -10
  87. package/dist/runtime/components/tools/ToolProperties.vue +7 -5
  88. package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
  89. package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
  90. package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
  91. package/dist/runtime/components/tools/ZoomControls.vue +17 -9
  92. package/dist/runtime/composables/canvas-limits.d.ts +22 -0
  93. package/dist/runtime/composables/canvas-limits.js +10 -0
  94. package/dist/runtime/composables/useAnnotationEngine.js +3 -1
  95. package/dist/runtime/composables/useFormFields.d.ts +6 -0
  96. package/dist/runtime/composables/useFormFields.js +28 -0
  97. package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
  98. package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
  99. package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
  100. package/dist/runtime/composables/useKviewerI18n.js +29 -0
  101. package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
  102. package/dist/runtime/composables/useScriptingManager.js +2 -0
  103. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  104. package/dist/runtime/composables/useViewerState.js +8 -0
  105. package/dist/runtime/i18n/messages.d.ts +252 -0
  106. package/dist/runtime/i18n/messages.js +249 -0
  107. package/dist/runtime/public-types.d.ts +2 -0
  108. package/dist/runtime/toolbar-tools.d.ts +54 -0
  109. package/dist/runtime/toolbar-tools.js +79 -0
  110. package/dist/types.d.mts +1 -1
  111. package/package.json +14 -31
@@ -15,6 +15,9 @@ export class Store {
15
15
  if (isOriginal) {
16
16
  this.originalAnnotationStore.set(store.id, store);
17
17
  }
18
+ if (this.originalAnnotationStore.has(store.id)) {
19
+ this.deletedOriginalIds.delete(store.id);
20
+ }
18
21
  return store;
19
22
  }
20
23
  update(id, updates) {
@@ -30,7 +30,6 @@ export class EditorArrow extends Editor {
30
30
  // Set the hit detection width
31
31
  lineCap: "round",
32
32
  lineJoin: "round",
33
- strokeScaleEnabled: false,
34
33
  visible: false,
35
34
  opacity: this.currentAnnotation.style.opacity
36
35
  });
@@ -34,8 +34,6 @@ export class EditorCircle extends Editor {
34
34
  radiusY: 0,
35
35
  x: pos.x,
36
36
  y: pos.y,
37
- // do not scale strokes
38
- strokeScaleEnabled: false,
39
37
  visible: false,
40
38
  // Start hidden
41
39
  stroke: this.currentAnnotation.style.color,
@@ -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
- title: string;
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
- title: string;
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;
@@ -0,0 +1,5 @@
1
+ import { AnnotationFlags, PDFNumber } from "pdf-lib";
2
+ export const PRINT_FLAG = PDFNumber.of(AnnotationFlags.Print);
3
+ export const PRINT_LOCKED_FLAG = PDFNumber.of(
4
+ AnnotationFlags.Print | AnnotationFlags.Locked
5
+ );
@@ -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
- F: 1 << AnnotationFlags.Print,
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
- await parseAnnotationToPdf(annotation, page, pdfDoc);
300
+ const isModifiedOriginal = originalAnnotationIds?.has(annotation.id) ?? false;
301
+ await parseAnnotationToPdf(annotation, page, pdfDoc, isModifiedOriginal);
300
302
  }
301
303
  }
302
- return pdfDoc.save();
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
- constructor(pdfDoc: PDFDocument, page: PDFPage, annotation: IAnnotationStore);
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
- constructor(pdfDoc, page, annotation) {
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
- const UNKNOWN_USER = "Unknown user";
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
- T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
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
- T: stringToPDFHexString(comment.title || UNKNOWN_USER),
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
- const UNKNOWN_USER = "Unknown user";
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
- `/Helvetica ${formatPdfNumber(fontSize)} Tf`,
87
+ `/${fontResourceName} ${formatPdfNumber(fontSize)} Tf`,
82
88
  `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`,
83
89
  `${formatPdfNumber(lineX)} ${formatPdfNumber(lineY)} Td`,
84
- `(${escapePdfString(line)}) Tj`,
90
+ showText,
85
91
  "ET"
86
92
  );
87
93
  }
88
94
  commands.push("Q");
89
- const fontDict = context.obj({
90
- Type: PDFName.of("Font"),
91
- Subtype: PDFName.of("Type1"),
92
- BaseFont: PDFName.of("Helvetica"),
93
- Encoding: PDFName.of("WinAnsiEncoding")
94
- });
95
- const fontRef = context.register(fontDict);
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({ Helvetica: fontRef })
119
+ Font: context.obj({ [fontResourceName]: fontResourceRef })
109
120
  })
110
121
  }),
111
- new TextEncoder().encode(commands.join("\n"))
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
- T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
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
- T: stringToPDFHexString(comment.title || UNKNOWN_USER),
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
- const UNKNOWN_USER = "Unknown user";
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
- T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
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
- T: stringToPDFHexString(comment.title || UNKNOWN_USER),
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
- const UNKNOWN_USER = "Unknown user";
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 strokeWidth = firstLine.strokeWidth ?? 1;
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
- T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
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
- T: stringToPDFHexString(comment.title || UNKNOWN_USER),
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
- const UNKNOWN_USER = "Unknown user";
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 strokeWidth = firstLine.strokeWidth ?? 1;
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
- T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
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
- T: stringToPDFHexString(comment.title || UNKNOWN_USER),
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"),