kviewer 0.2.0 → 0.2.2

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 (114) hide show
  1. package/README.md +47 -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 +5 -0
  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/painter.d.ts +31 -0
  12. package/dist/runtime/annotation/engine/painter.js +88 -2
  13. package/dist/runtime/annotation/engine/store.js +3 -0
  14. package/dist/runtime/annotation/engine/tools/arrow.js +0 -1
  15. package/dist/runtime/annotation/engine/tools/circle.js +0 -2
  16. package/dist/runtime/annotation/engine/tools/free-highlight.js +0 -2
  17. package/dist/runtime/annotation/engine/tools/freehand.js +0 -2
  18. package/dist/runtime/annotation/engine/tools/rectangle.js +0 -2
  19. package/dist/runtime/annotation/engine/types.d.ts +5 -0
  20. package/dist/runtime/annotation/font-style.d.ts +15 -0
  21. package/dist/runtime/annotation/font-style.js +19 -0
  22. package/dist/runtime/annotation/parsers/extractCheckboxStyles.d.ts +8 -0
  23. package/dist/runtime/annotation/parsers/extractCheckboxStyles.js +24 -1
  24. package/dist/runtime/annotation/pdf-export/annotation-flags.d.ts +8 -0
  25. package/dist/runtime/annotation/pdf-export/annotation-flags.js +5 -0
  26. package/dist/runtime/annotation/pdf-export/export-form-fields.js +3 -1
  27. package/dist/runtime/annotation/pdf-export/export.js +13 -1
  28. package/dist/runtime/annotation/pdf-export/parse_circle.js +3 -0
  29. package/dist/runtime/annotation/pdf-export/parse_freetext.js +31 -19
  30. package/dist/runtime/annotation/pdf-export/parse_highlight.js +3 -0
  31. package/dist/runtime/annotation/pdf-export/parse_ink.js +5 -1
  32. package/dist/runtime/annotation/pdf-export/parse_line.js +5 -1
  33. package/dist/runtime/annotation/pdf-export/parse_polyline.js +5 -1
  34. package/dist/runtime/annotation/pdf-export/parse_square.js +3 -0
  35. package/dist/runtime/annotation/pdf-export/parse_stamp.js +4 -2
  36. package/dist/runtime/annotation/pdf-export/parse_strikeout.js +3 -0
  37. package/dist/runtime/annotation/pdf-export/parse_text.js +3 -0
  38. package/dist/runtime/annotation/pdf-export/parse_underline.js +3 -0
  39. package/dist/runtime/annotation/pdf-export/unicode-font.d.ts +5 -0
  40. package/dist/runtime/annotation/pdf-export/unicode-font.js +31 -0
  41. package/dist/runtime/annotation/pdf-export/winansi.d.ts +17 -0
  42. package/dist/runtime/annotation/pdf-export/winansi.js +77 -0
  43. package/dist/runtime/annotation/pdf-import/decode_circle.js +0 -1
  44. package/dist/runtime/annotation/pdf-import/decode_ink.js +0 -1
  45. package/dist/runtime/annotation/pdf-import/decode_line.js +0 -1
  46. package/dist/runtime/annotation/pdf-import/decode_square.js +0 -1
  47. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.d.ts +12 -0
  48. package/dist/runtime/annotation/pdf-import/extract_stamp_appearance.js +17 -4
  49. package/dist/runtime/annotation/pdf-import/types.d.ts +2 -0
  50. package/dist/runtime/assets/fonts/LICENSE-LiberationSans +102 -0
  51. package/dist/runtime/assets/fonts/LiberationSans-Regular.ttf +0 -0
  52. package/dist/runtime/assets/kviewer.css +1 -1
  53. package/dist/runtime/components/AnnotationToolbar.vue +6 -4
  54. package/dist/runtime/components/PdfPage.d.vue.ts +16 -0
  55. package/dist/runtime/components/PdfPage.vue +170 -9
  56. package/dist/runtime/components/PdfPage.vue.d.ts +16 -0
  57. package/dist/runtime/components/PerfHud.d.vue.ts +3 -0
  58. package/dist/runtime/components/PerfHud.vue +97 -0
  59. package/dist/runtime/components/PerfHud.vue.d.ts +3 -0
  60. package/dist/runtime/components/ToolButton.vue +4 -1
  61. package/dist/runtime/components/Viewer.d.vue.ts +37 -2
  62. package/dist/runtime/components/Viewer.vue +208 -13
  63. package/dist/runtime/components/Viewer.vue.d.ts +37 -2
  64. package/dist/runtime/components/ViewerBar.d.vue.ts +21 -1
  65. package/dist/runtime/components/ViewerBar.vue +183 -101
  66. package/dist/runtime/components/ViewerBar.vue.d.ts +21 -1
  67. package/dist/runtime/components/ViewerTabs.d.vue.ts +10 -2
  68. package/dist/runtime/components/ViewerTabs.vue +13 -2
  69. package/dist/runtime/components/ViewerTabs.vue.d.ts +10 -2
  70. package/dist/runtime/components/form-fields/FormDropdown.vue +50 -19
  71. package/dist/runtime/components/form-fields/FormSignatureField.vue +4 -2
  72. package/dist/runtime/components/form-fields/FormTextField.vue +7 -18
  73. package/dist/runtime/components/modals/FreeTextModal.vue +6 -4
  74. package/dist/runtime/components/modals/SignatureDrawModal.vue +6 -4
  75. package/dist/runtime/components/panels/PlacedFieldSidebar.vue +67 -50
  76. package/dist/runtime/components/panels/SignaturePicker.vue +6 -4
  77. package/dist/runtime/components/panels/StampPicker.vue +3 -1
  78. package/dist/runtime/components/tools/AnnotationToolButton.d.vue.ts +10 -0
  79. package/dist/runtime/components/tools/AnnotationToolButton.vue +33 -0
  80. package/dist/runtime/components/tools/AnnotationToolButton.vue.d.ts +10 -0
  81. package/dist/runtime/components/tools/MarqueeTool.vue +3 -1
  82. package/dist/runtime/components/tools/PageSettings.vue +13 -11
  83. package/dist/runtime/components/tools/SearchTool.vue +12 -10
  84. package/dist/runtime/components/tools/ToolProperties.vue +7 -5
  85. package/dist/runtime/components/tools/ToolbarMenu.d.vue.ts +9 -0
  86. package/dist/runtime/components/tools/ToolbarMenu.vue +79 -0
  87. package/dist/runtime/components/tools/ToolbarMenu.vue.d.ts +9 -0
  88. package/dist/runtime/components/tools/ZoomControls.vue +17 -9
  89. package/dist/runtime/composables/canvas-limits.d.ts +22 -0
  90. package/dist/runtime/composables/canvas-limits.js +10 -0
  91. package/dist/runtime/composables/useFormFields.d.ts +6 -0
  92. package/dist/runtime/composables/useFormFields.js +28 -0
  93. package/dist/runtime/composables/useInertiaPanzoom.d.ts +10 -0
  94. package/dist/runtime/composables/useInertiaPanzoom.js +57 -10
  95. package/dist/runtime/composables/useKviewerI18n.d.ts +18 -0
  96. package/dist/runtime/composables/useKviewerI18n.js +29 -0
  97. package/dist/runtime/composables/usePageVirtualization.d.ts +11 -0
  98. package/dist/runtime/composables/usePageVirtualization.js +40 -0
  99. package/dist/runtime/composables/useScriptingManager.d.ts +8 -4
  100. package/dist/runtime/composables/useScriptingManager.js +2 -0
  101. package/dist/runtime/composables/useViewerState.d.ts +4 -0
  102. package/dist/runtime/composables/useViewerState.js +8 -0
  103. package/dist/runtime/embed/bridge-client.d.ts +9 -1
  104. package/dist/runtime/embed/bridge-client.js +13 -0
  105. package/dist/runtime/embed/bridge-host.d.ts +3 -0
  106. package/dist/runtime/embed/bridge-host.js +30 -8
  107. package/dist/runtime/embed/protocol.d.ts +21 -0
  108. package/dist/runtime/i18n/messages.d.ts +252 -0
  109. package/dist/runtime/i18n/messages.js +249 -0
  110. package/dist/runtime/public-types.d.ts +2 -0
  111. package/dist/runtime/toolbar-tools.d.ts +54 -0
  112. package/dist/runtime/toolbar-tools.js +79 -0
  113. package/dist/types.d.mts +1 -1
  114. package/package.json +14 -31
@@ -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,
@@ -192,6 +192,11 @@ export interface FormFieldDefinition {
192
192
  /** Signature-only: placeholder text shown on the unsigned widget.
193
193
  * Defaults to 'Click to sign' when unset. */
194
194
  promptText?: string;
195
+ /** Signature-only: the source PDF already carries a digital signature
196
+ * (`/V` is a `/Sig` dict) on this widget. The widget's appearance
197
+ * stream is rasterized after parse and seeded as the field value so
198
+ * the existing stamp is shown instead of the sign prompt. */
199
+ signedInSource?: boolean;
195
200
  /** Signature-only: when this signature is filled, lock other fields in
196
201
  * the form so they can no longer be edited. Mirrors PDF spec
197
202
  * /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";
@@ -299,5 +300,16 @@ export async function exportAnnotationsToPdf({
299
300
  await parseAnnotationToPdf(annotation, page, pdfDoc);
300
301
  }
301
302
  }
302
- return pdfDoc.save();
303
+ return savePdf(pdfDoc);
304
+ }
305
+ async function savePdf(pdfDoc) {
306
+ try {
307
+ return await pdfDoc.save();
308
+ } catch (err) {
309
+ const message = err instanceof Error ? err.message : "";
310
+ if (!/cannot encode/i.test(message)) throw err;
311
+ const font = await getUnicodeFont(pdfDoc);
312
+ pdfDoc.getForm().updateFieldAppearances(font);
313
+ return pdfDoc.save({ updateFieldAppearances: false });
314
+ }
303
315
  }
@@ -3,6 +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
+ import { PRINT_FLAG } from "./annotation-flags.js";
6
7
  const UNKNOWN_USER = "Unknown user";
7
8
  export class CircleParser extends AnnotationParser {
8
9
  async parse() {
@@ -14,6 +15,7 @@ export class CircleParser extends AnnotationParser {
14
15
  const mainAnn = context.obj({
15
16
  Type: PDFName.of("Annot"),
16
17
  Subtype: PDFName.of("Circle"),
18
+ F: PRINT_FLAG,
17
19
  Rect: rect,
18
20
  C: rgbToPdfColor(annotation.color || void 0),
19
21
  T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
@@ -28,6 +30,7 @@ export class CircleParser extends AnnotationParser {
28
30
  const replyAnn = context.obj({
29
31
  Type: PDFName.of("Annot"),
30
32
  Subtype: PDFName.of("Text"),
33
+ F: PRINT_FLAG,
31
34
  Rect: rect,
32
35
  Contents: stringToPDFHexString(comment.content),
33
36
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -3,6 +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
+ import { encodeWinAnsi, isWinAnsiEncodable } from "./winansi.js";
7
+ import { getUnicodeFont } from "./unicode-font.js";
8
+ import { PRINT_FLAG } from "./annotation-flags.js";
6
9
  const UNKNOWN_USER = "Unknown user";
7
10
  function formatPdfNumber(value) {
8
11
  if (!Number.isFinite(value)) return "0";
@@ -63,41 +66,50 @@ export class FreeTextParser extends AnnotationParser {
63
66
  ];
64
67
  const appearanceWidth = Math.max(1, textWidth);
65
68
  const appearanceHeight = Math.max(1, textHeight);
69
+ const useUnicodeFont = !isWinAnsiEncodable(text);
70
+ let unicodeFont = null;
71
+ if (useUnicodeFont) {
72
+ unicodeFont = await getUnicodeFont(pdfDoc);
73
+ }
74
+ const fontResourceName = useUnicodeFont ? "FUni" : "Helvetica";
66
75
  const commands = ["q"];
67
76
  for (let i = 0; i < lines.length; i++) {
68
77
  const line = lines[i] ?? "";
69
78
  if (line.length === 0 && i < lines.length - 1) continue;
70
79
  const lineY = appearanceHeight - (i + 0.8) * lineHeight;
71
80
  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);
81
+ if (quadding === 1 || quadding === 2) {
82
+ const lineWidth = unicodeFont ? unicodeFont.widthOfTextAtSize(line, fontSize) : line.length * estimatedCharWidth;
83
+ lineX = quadding === 1 ? Math.max(0, (appearanceWidth - lineWidth) / 2) : Math.max(0, appearanceWidth - lineWidth);
78
84
  }
85
+ const showText = unicodeFont ? `${unicodeFont.encodeText(line).toString()} Tj` : `(${escapePdfString(line)}) Tj`;
79
86
  commands.push(
80
87
  "BT",
81
- `/Helvetica ${formatPdfNumber(fontSize)} Tf`,
88
+ `/${fontResourceName} ${formatPdfNumber(fontSize)} Tf`,
82
89
  `${formatPdfNumber(r)} ${formatPdfNumber(g)} ${formatPdfNumber(b)} rg`,
83
90
  `${formatPdfNumber(lineX)} ${formatPdfNumber(lineY)} Td`,
84
- `(${escapePdfString(line)}) Tj`,
91
+ showText,
85
92
  "ET"
86
93
  );
87
94
  }
88
95
  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);
96
+ let fontResourceRef;
97
+ if (unicodeFont) {
98
+ fontResourceRef = unicodeFont.ref;
99
+ } else {
100
+ fontResourceRef = context.register(context.obj({
101
+ Type: PDFName.of("Font"),
102
+ Subtype: PDFName.of("Type1"),
103
+ BaseFont: PDFName.of("Helvetica"),
104
+ Encoding: PDFName.of("WinAnsiEncoding")
105
+ }));
106
+ }
96
107
  const appearanceMatrix = formAppearanceMatrix(
97
108
  pageTransform.rotation,
98
109
  appearanceWidth,
99
110
  appearanceHeight
100
111
  );
112
+ const streamBytes = unicodeFont ? new TextEncoder().encode(commands.join("\n")) : encodeWinAnsi(commands.join("\n"));
101
113
  const appearanceStream = PDFRawStream.of(
102
114
  context.obj({
103
115
  Type: PDFName.of("XObject"),
@@ -105,10 +117,10 @@ export class FreeTextParser extends AnnotationParser {
105
117
  BBox: context.obj([0, 0, appearanceWidth, appearanceHeight]),
106
118
  Matrix: context.obj(appearanceMatrix.map((n) => PDFNumber.of(n))),
107
119
  Resources: context.obj({
108
- Font: context.obj({ Helvetica: fontRef })
120
+ Font: context.obj({ [fontResourceName]: fontResourceRef })
109
121
  })
110
122
  }),
111
- new TextEncoder().encode(commands.join("\n"))
123
+ streamBytes
112
124
  );
113
125
  const appearanceRef = context.register(appearanceStream);
114
126
  const ap = context.obj({ N: appearanceRef });
@@ -116,6 +128,7 @@ export class FreeTextParser extends AnnotationParser {
116
128
  const mainAnn = context.obj({
117
129
  Type: PDFName.of("Annot"),
118
130
  Subtype: PDFName.of("FreeText"),
131
+ F: PRINT_FLAG,
119
132
  Rect: rect,
120
133
  NM: PDFString.of(annotation.id),
121
134
  Contents: stringToPDFHexString(text),
@@ -125,8 +138,6 @@ export class FreeTextParser extends AnnotationParser {
125
138
  M: PDFString.of(annotation.date || ""),
126
139
  C: context.obj(rgbToPdfColor(annotation.color || void 0)),
127
140
  Border: context.obj([0, 0, 0]),
128
- F: PDFNumber.of(4),
129
- // Print flag
130
141
  AP: ap
131
142
  });
132
143
  const mainAnnRef = context.register(mainAnn);
@@ -135,6 +146,7 @@ export class FreeTextParser extends AnnotationParser {
135
146
  const replyAnn = context.obj({
136
147
  Type: PDFName.of("Annot"),
137
148
  Subtype: PDFName.of("Text"),
149
+ F: PRINT_FLAG,
138
150
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
139
151
  Contents: stringToPDFHexString(comment.content),
140
152
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -6,6 +6,7 @@ import {
6
6
  pageTransformFromPdfLibPage
7
7
  } from "../pdf-import/transform.js";
8
8
  import { AnnotationParser } from "./parse.js";
9
+ import { PRINT_FLAG } from "./annotation-flags.js";
9
10
  const UNKNOWN_USER = "Unknown user";
10
11
  function formatPdfNumber(value) {
11
12
  if (!Number.isFinite(value)) return "0";
@@ -115,6 +116,7 @@ export class HighlightParser extends AnnotationParser {
115
116
  const mainAnn = context.obj({
116
117
  Type: PDFName.of("Annot"),
117
118
  Subtype: PDFName.of("Highlight"),
119
+ F: PRINT_FLAG,
118
120
  Rect: rect,
119
121
  QuadPoints: context.obj(quadPoints),
120
122
  C: context.obj(color),
@@ -131,6 +133,7 @@ export class HighlightParser extends AnnotationParser {
131
133
  const replyAnn = context.obj({
132
134
  Type: PDFName.of("Annot"),
133
135
  Subtype: PDFName.of("Text"),
136
+ F: PRINT_FLAG,
134
137
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
135
138
  Contents: stringToPDFHexString(comment.content),
136
139
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -2,6 +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
+ import { PRINT_FLAG } from "./annotation-flags.js";
5
6
  const UNKNOWN_USER = "Unknown user";
6
7
  function formatPdfNumber(value) {
7
8
  if (!Number.isFinite(value)) return "0";
@@ -45,7 +46,8 @@ export class InkParser extends AnnotationParser {
45
46
  transformedPaths.map((path) => context.obj(path))
46
47
  );
47
48
  const firstLine = lines[0]?.attrs || {};
48
- const strokeWidth = firstLine.strokeWidth ?? 1;
49
+ const strokeScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
50
+ const strokeWidth = (firstLine.strokeWidth ?? 1) * strokeScale;
49
51
  const opacity = firstLine.opacity ?? 1;
50
52
  const color = firstLine.stroke ?? annotation.color ?? "rgb(255, 0, 0)";
51
53
  const [r, g, b] = rgbToPdfColor(color);
@@ -120,6 +122,7 @@ export class InkParser extends AnnotationParser {
120
122
  const mainAnn = context.obj({
121
123
  Type: PDFName.of("Annot"),
122
124
  Subtype: PDFName.of("Ink"),
125
+ F: PRINT_FLAG,
123
126
  Rect: rect,
124
127
  InkList: inkList,
125
128
  C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
@@ -138,6 +141,7 @@ export class InkParser extends AnnotationParser {
138
141
  const replyAnn = context.obj({
139
142
  Type: PDFName.of("Annot"),
140
143
  Subtype: PDFName.of("Text"),
144
+ F: PRINT_FLAG,
141
145
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
142
146
  Contents: stringToPDFHexString(comment.content),
143
147
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -2,6 +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
+ import { PRINT_FLAG } from "./annotation-flags.js";
5
6
  const UNKNOWN_USER = "Unknown user";
6
7
  function formatPdfNumber(value) {
7
8
  if (!Number.isFinite(value)) return "0";
@@ -39,7 +40,8 @@ export class LineParser extends AnnotationParser {
39
40
  transformedPaths.map((path) => context.obj(path))
40
41
  );
41
42
  const firstLine = lines[0]?.attrs || {};
42
- const strokeWidth = firstLine.strokeWidth ?? 1;
43
+ const strokeScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
44
+ const strokeWidth = (firstLine.strokeWidth ?? 1) * strokeScale;
43
45
  const opacity = firstLine.opacity ?? 1;
44
46
  const color = firstLine.stroke ?? annotation.color ?? "rgb(255, 0, 0)";
45
47
  const [r, g, b] = rgbToPdfColor(color);
@@ -158,6 +160,7 @@ export class LineParser extends AnnotationParser {
158
160
  const mainAnn = context.obj({
159
161
  Type: PDFName.of("Annot"),
160
162
  Subtype: PDFName.of("Ink"),
163
+ F: PRINT_FLAG,
161
164
  Rect: rect,
162
165
  InkList: inkList,
163
166
  C: context.obj([PDFNumber.of(r), PDFNumber.of(g), PDFNumber.of(b)]),
@@ -176,6 +179,7 @@ export class LineParser extends AnnotationParser {
176
179
  const replyAnn = context.obj({
177
180
  Type: PDFName.of("Annot"),
178
181
  Subtype: PDFName.of("Text"),
182
+ F: PRINT_FLAG,
179
183
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
180
184
  Contents: stringToPDFHexString(comment.content),
181
185
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -3,6 +3,7 @@ import { convertKonvaRectToPdfRect, rgbToPdfColor, stringToPDFHexString } from "
3
3
  import { convertCanvasPointToPdf, pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
4
4
  import { AnnotationParser } from "./parse.js";
5
5
  import { computeBoundsFromKonvaString } from "./parse_utils.js";
6
+ import { PRINT_FLAG } from "./annotation-flags.js";
6
7
  const UNKNOWN_USER = "Unknown user";
7
8
  function parseSvgPathToPoints(data) {
8
9
  const commands = data.match(/[a-z][^a-z]*/gi) || [];
@@ -51,7 +52,8 @@ export class PolylineParser extends AnnotationParser {
51
52
  })
52
53
  );
53
54
  const firstLine = lines[0]?.attrs || {};
54
- const strokeWidth = firstLine.strokeWidth ?? 1;
55
+ const strokeScale = (Math.abs(scaleX) + Math.abs(scaleY)) / 2;
56
+ const strokeWidth = (firstLine.strokeWidth ?? 1) * strokeScale;
55
57
  const opacity = firstLine.opacity ?? 1;
56
58
  const color = firstLine.stroke ?? annotation.color ?? "rgb(255, 0, 0)";
57
59
  const [r, g, b] = rgbToPdfColor(color);
@@ -62,6 +64,7 @@ export class PolylineParser extends AnnotationParser {
62
64
  const mainAnn = context.obj({
63
65
  Type: PDFName.of("Annot"),
64
66
  Subtype: PDFName.of("Ink"),
67
+ F: PRINT_FLAG,
65
68
  // Compute Rect from konvaString so it reflects the current position
66
69
  // even when konvaClientRect is stale after a move/resize.
67
70
  Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageTransform),
@@ -81,6 +84,7 @@ export class PolylineParser extends AnnotationParser {
81
84
  const replyAnn = context.obj({
82
85
  Type: PDFName.of("Annot"),
83
86
  Subtype: PDFName.of("Text"),
87
+ F: PRINT_FLAG,
84
88
  Rect: convertKonvaRectToPdfRect(computeBoundsFromKonvaString(annotation.konvaString) ?? annotation.konvaClientRect, pageTransform),
85
89
  Contents: stringToPDFHexString(comment.content),
86
90
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -3,6 +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
+ import { PRINT_FLAG } from "./annotation-flags.js";
6
7
  const UNKNOWN_USER = "Unknown user";
7
8
  export class SquareParser extends AnnotationParser {
8
9
  async parse() {
@@ -14,6 +15,7 @@ export class SquareParser extends AnnotationParser {
14
15
  const mainAnn = context.obj({
15
16
  Type: PDFName.of("Annot"),
16
17
  Subtype: PDFName.of("Square"),
18
+ F: PRINT_FLAG,
17
19
  Rect: rect,
18
20
  C: rgbToPdfColor(annotation.color || void 0),
19
21
  T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
@@ -28,6 +30,7 @@ export class SquareParser extends AnnotationParser {
28
30
  const replyAnn = context.obj({
29
31
  Type: PDFName.of("Annot"),
30
32
  Subtype: PDFName.of("Text"),
33
+ F: PRINT_FLAG,
31
34
  Rect: rect,
32
35
  Contents: stringToPDFHexString(comment.content),
33
36
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -3,6 +3,7 @@ import { convertKonvaRectToPdfRect, stringToPDFHexString } from "../engine/utils
3
3
  import { pageTransformFromPdfLibPage } from "../pdf-import/transform.js";
4
4
  import { AnnotationParser } from "./parse.js";
5
5
  import { computeBoundsFromKonvaString, formAppearanceMatrix } from "./parse_utils.js";
6
+ import { PRINT_FLAG, PRINT_LOCKED_FLAG } from "./annotation-flags.js";
6
7
  const UNKNOWN_USER = "Unknown user";
7
8
  const SVG_RASTER_SCALE = 4;
8
9
  const MAX_RASTER_EDGE = 4096;
@@ -170,13 +171,13 @@ export class StampParser extends AnnotationParser {
170
171
  const stampAnnDict = {
171
172
  Type: PDFName.of("Annot"),
172
173
  Subtype: PDFName.of("Stamp"),
174
+ F: PRINT_LOCKED_FLAG,
173
175
  Rect: rect,
174
176
  NM: PDFString.of(annotation.id),
175
177
  Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
176
178
  T: stringToPDFHexString(annotation.title || UNKNOWN_USER),
177
179
  M: PDFString.of(annotation.date || ""),
178
- Open: false,
179
- F: PDFNumber.of(128)
180
+ Open: false
180
181
  };
181
182
  if (apDict) {
182
183
  stampAnnDict.AP = apDict;
@@ -188,6 +189,7 @@ export class StampParser extends AnnotationParser {
188
189
  const replyAnn = context.obj({
189
190
  Type: PDFName.of("Annot"),
190
191
  Subtype: PDFName.of("Text"),
192
+ F: PRINT_FLAG,
191
193
  Rect: convertKonvaRectToPdfRect(bounds ?? annotation.konvaClientRect, pageTransform),
192
194
  Contents: stringToPDFHexString(comment.content),
193
195
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -2,6 +2,7 @@ import { PDFName, 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
+ import { PRINT_FLAG } from "./annotation-flags.js";
5
6
  const UNKNOWN_USER = "Unknown user";
6
7
  function buildQuadPoints(konvaString, t) {
7
8
  const konvaGroup = JSON.parse(konvaString);
@@ -29,6 +30,7 @@ export class StrikeOutParser extends AnnotationParser {
29
30
  const mainAnn = context.obj({
30
31
  Type: PDFName.of("Annot"),
31
32
  Subtype: PDFName.of("StrikeOut"),
33
+ F: PRINT_FLAG,
32
34
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
33
35
  QuadPoints: quadPoints,
34
36
  C: rgbToPdfColor(annotation.color || void 0),
@@ -43,6 +45,7 @@ export class StrikeOutParser extends AnnotationParser {
43
45
  const replyAnn = context.obj({
44
46
  Type: PDFName.of("Annot"),
45
47
  Subtype: PDFName.of("Text"),
48
+ F: PRINT_FLAG,
46
49
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
47
50
  Contents: stringToPDFHexString(comment.content),
48
51
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -3,6 +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
+ import { PRINT_FLAG } from "./annotation-flags.js";
6
7
  const UNKNOWN_USER = "Unknown user";
7
8
  export class TextParser extends AnnotationParser {
8
9
  async parse() {
@@ -14,6 +15,7 @@ export class TextParser extends AnnotationParser {
14
15
  const mainAnn = context.obj({
15
16
  Type: PDFName.of("Annot"),
16
17
  Subtype: PDFName.of("Text"),
18
+ F: PRINT_FLAG,
17
19
  Rect: rect,
18
20
  NM: PDFString.of(annotation.id),
19
21
  Contents: stringToPDFHexString(annotation.contentsObj?.text || ""),
@@ -29,6 +31,7 @@ export class TextParser extends AnnotationParser {
29
31
  const replyAnn = context.obj({
30
32
  Type: PDFName.of("Annot"),
31
33
  Subtype: PDFName.of("Text"),
34
+ F: PRINT_FLAG,
32
35
  Rect: rect,
33
36
  Contents: stringToPDFHexString(comment.content),
34
37
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -2,6 +2,7 @@ import { PDFName, 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
+ import { PRINT_FLAG } from "./annotation-flags.js";
5
6
  const UNKNOWN_USER = "Unknown user";
6
7
  function buildQuadPoints(konvaString, t) {
7
8
  const konvaGroup = JSON.parse(konvaString);
@@ -29,6 +30,7 @@ export class UnderlineParser extends AnnotationParser {
29
30
  const mainAnn = context.obj({
30
31
  Type: PDFName.of("Annot"),
31
32
  Subtype: PDFName.of("Underline"),
33
+ F: PRINT_FLAG,
32
34
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
33
35
  QuadPoints: quadPoints,
34
36
  C: rgbToPdfColor(annotation.color || void 0),
@@ -43,6 +45,7 @@ export class UnderlineParser extends AnnotationParser {
43
45
  const replyAnn = context.obj({
44
46
  Type: PDFName.of("Annot"),
45
47
  Subtype: PDFName.of("Text"),
48
+ F: PRINT_FLAG,
46
49
  Rect: convertKonvaRectToPdfRect(annotation.konvaClientRect, pageTransform),
47
50
  Contents: stringToPDFHexString(comment.content),
48
51
  T: stringToPDFHexString(comment.title || UNKNOWN_USER),
@@ -0,0 +1,5 @@
1
+ import type { PDFDocument, PDFFont } from 'pdf-lib';
2
+ /** Test/SSR hook: replace how the raw font bytes are obtained. */
3
+ export declare function setUnicodeFontLoader(loader: () => Promise<Uint8Array>): void;
4
+ /** Get (or embed once) the Unicode fallback font for this document. */
5
+ export declare function getUnicodeFont(pdfDoc: PDFDocument): Promise<PDFFont>;
@@ -0,0 +1,31 @@
1
+ import fontkit from "@pdf-lib/fontkit";
2
+ let fontBytesPromise = null;
3
+ let loadFontBytes = async () => {
4
+ const { default: fontUrl } = await import(
5
+ // @ts-expect-error -- Vite asset import, no type declaration
6
+ "../../assets/fonts/LiberationSans-Regular.ttf?url"
7
+ );
8
+ const response = await fetch(fontUrl);
9
+ if (!response.ok) {
10
+ throw new Error(`Failed to load Unicode fallback font (${response.status})`);
11
+ }
12
+ return new Uint8Array(await response.arrayBuffer());
13
+ };
14
+ export function setUnicodeFontLoader(loader) {
15
+ loadFontBytes = loader;
16
+ fontBytesPromise = null;
17
+ }
18
+ const fontCache = /* @__PURE__ */ new WeakMap();
19
+ export function getUnicodeFont(pdfDoc) {
20
+ let cached = fontCache.get(pdfDoc);
21
+ if (!cached) {
22
+ cached = (async () => {
23
+ fontBytesPromise ??= loadFontBytes();
24
+ const bytes = await fontBytesPromise;
25
+ pdfDoc.registerFontkit(fontkit);
26
+ return pdfDoc.embedFont(bytes, { subset: true });
27
+ })();
28
+ fontCache.set(pdfDoc, cached);
29
+ }
30
+ return cached;
31
+ }