kviewer 0.0.11 → 0.1.1

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 (45) hide show
  1. package/dist/module.d.mts +9 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +22 -2
  4. package/dist/runtime/annotation/engine/painter.d.ts +8 -1
  5. package/dist/runtime/annotation/engine/painter.js +11 -6
  6. package/dist/runtime/annotation/engine/tools/form-field.d.ts +6 -0
  7. package/dist/runtime/annotation/engine/tools/form-field.js +3 -1
  8. package/dist/runtime/annotation/engine/types.d.ts +64 -0
  9. package/dist/runtime/annotation/pdf-export/export-form-fields.js +102 -52
  10. package/dist/runtime/assets/kviewer.css +1 -1
  11. package/dist/runtime/components/PdfPage.vue +6 -1
  12. package/dist/runtime/components/Viewer.d.vue.ts +41 -1
  13. package/dist/runtime/components/Viewer.vue +84 -3
  14. package/dist/runtime/components/Viewer.vue.d.ts +41 -1
  15. package/dist/runtime/components/ViewerBar.d.vue.ts +7 -1
  16. package/dist/runtime/components/ViewerBar.vue +36 -0
  17. package/dist/runtime/components/ViewerBar.vue.d.ts +7 -1
  18. package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -6
  19. package/dist/runtime/components/ViewerTabs.vue +11 -2
  20. package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -6
  21. package/dist/runtime/components/form-fields/FormFieldWrapper.vue +10 -2
  22. package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +1 -1
  23. package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
  24. package/dist/runtime/composables/useAnnotationEngine.js +2 -1
  25. package/dist/runtime/composables/useFormFields.d.ts +40 -3
  26. package/dist/runtime/composables/useFormFields.js +101 -1
  27. package/dist/runtime/composables/useInertiaPanzoom.js +1 -0
  28. package/dist/runtime/composables/usePageProxyCache.d.ts +4 -0
  29. package/dist/runtime/composables/usePageProxyCache.js +4 -1
  30. package/dist/runtime/composables/useScriptingBridge.d.ts +29 -0
  31. package/dist/runtime/composables/useScriptingBridge.js +74 -0
  32. package/dist/runtime/composables/useScriptingManager.d.ts +65 -0
  33. package/dist/runtime/composables/useScriptingManager.js +123 -0
  34. package/dist/runtime/embed/bridge-client.d.ts +58 -0
  35. package/dist/runtime/embed/bridge-client.js +143 -0
  36. package/dist/runtime/embed/bridge-host.d.ts +59 -0
  37. package/dist/runtime/embed/bridge-host.js +136 -0
  38. package/dist/runtime/embed/protocol.d.ts +105 -0
  39. package/dist/runtime/embed/protocol.js +13 -0
  40. package/dist/runtime/menu-items.d.ts +34 -0
  41. package/dist/runtime/menu-items.js +0 -0
  42. package/dist/runtime/public-types.d.ts +8 -1
  43. package/dist/runtime/public-types.js +3 -0
  44. package/dist/types.d.mts +1 -1
  45. package/package.json +1 -1
package/dist/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- export { ExportPdfOptions, SignatureData, SignatureHandlers, ViewMode } from '../dist/runtime/public-types.js';
2
+ export { AddFormFieldPayload, CheckboxStyle, ExportPdfOptions, FormFieldDefinition, FormFieldOrigin, FormFieldType, FormFieldValue, KVIEWER_EMBED_PROTOCOL_VERSION, KViewerApi, KViewerEmbedBridgeOptions, KViewerEmbedClient, KViewerEmbedClientOptions, KViewerEmbedEventName, KViewerEmbedEventPayloads, KViewerEmbedMethodName, SignatureData, SignatureHandlers, ViewMode, ViewerMenuButtonItem, ViewerMenuCheckboxItem, ViewerMenuItem, ViewerMenuSeparatorItem, useKViewerEmbedBridge } from '../dist/runtime/public-types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -7,6 +7,14 @@ interface ModuleOptions {
7
7
  * @defaultValue `K`
8
8
  */
9
9
  prefix?: string;
10
+ /**
11
+ * Minimum pixel ratio used to rasterize PDF pages. The canvas is rendered
12
+ * at `cssSize * max(devicePixelRatio, minRenderPixelRatio)` and downscaled
13
+ * by CSS, producing a supersampled, sharper image on non-HiDPI displays.
14
+ * Higher values cost ~quadratically more memory and CPU.
15
+ * @defaultValue `2`
16
+ */
17
+ minRenderPixelRatio?: number;
10
18
  }
11
19
 
12
20
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.0.11",
4
+ "version": "0.1.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { dirname, join } from 'node:path';
3
- import { defineNuxtModule, createResolver, addPlugin, addComponentsDir } from '@nuxt/kit';
3
+ import { defineNuxtModule, createResolver, addPlugin, addComponentsDir, addImports } from '@nuxt/kit';
4
+ export { KVIEWER_EMBED_PROTOCOL_VERSION, KViewerEmbedClient, useKViewerEmbedBridge } from '../dist/runtime/public-types.js';
4
5
 
5
6
  const module$1 = defineNuxtModule({
6
7
  meta: {
@@ -9,10 +10,15 @@ const module$1 = defineNuxtModule({
9
10
  },
10
11
  // Default configuration options of the Nuxt module
11
12
  defaults: {
12
- prefix: "K"
13
+ prefix: "K",
14
+ minRenderPixelRatio: 2
13
15
  },
14
16
  setup(options, nuxt) {
15
17
  const { resolve } = createResolver(import.meta.url);
18
+ nuxt.options.runtimeConfig.public.kviewer = {
19
+ ...nuxt.options.runtimeConfig.public.kviewer,
20
+ minRenderPixelRatio: options.minRenderPixelRatio ?? 2
21
+ };
16
22
  nuxt.options.css.push("pdfjs-dist/web/pdf_viewer.css");
17
23
  nuxt.options.css.push(resolve("./runtime/assets/kviewer.css"));
18
24
  addPlugin(resolve("./runtime/plugin"));
@@ -22,6 +28,11 @@ const module$1 = defineNuxtModule({
22
28
  prefix: options.prefix,
23
29
  ignore: ["color-mode/**", "content/**", "prose/**"]
24
30
  });
31
+ addImports([
32
+ { name: "useKViewerEmbedBridge", from: resolve("./runtime/embed/bridge-host") },
33
+ { name: "KViewerEmbedClient", from: resolve("./runtime/embed/bridge-client") },
34
+ { name: "KVIEWER_EMBED_PROTOCOL_VERSION", from: resolve("./runtime/embed/protocol") }
35
+ ]);
25
36
  const require_ = createRequire(import.meta.url);
26
37
  const pdfjsDir = dirname(require_.resolve("pdfjs-dist/package.json"));
27
38
  nuxt.hook("nitro:config", (nitroConfig) => {
@@ -36,6 +47,15 @@ const module$1 = defineNuxtModule({
36
47
  dir: join(pdfjsDir, "cmaps"),
37
48
  baseURL: "/_kviewer/cmaps",
38
49
  maxAge: 60 * 60 * 24 * 365
50
+ },
51
+ // PDF.js scripting sandbox bundle. Required when the viewer's
52
+ // `scripting` prop is enabled — the sandbox is a QuickJS WASM
53
+ // module that executes embedded PDF JavaScript (field AA scripts,
54
+ // calculate/format/validate, document/page-open actions).
55
+ {
56
+ dir: join(pdfjsDir, "legacy/build"),
57
+ baseURL: "/_kviewer/pdfjs",
58
+ maxAge: 60 * 60 * 24 * 365
39
59
  }
40
60
  );
41
61
  });
@@ -23,6 +23,10 @@ export interface PainterCallbacks {
23
23
  onRequestTextInput: IEditorFreeTextOptions['onRequestTextInput'];
24
24
  onRequestDeleteConfirm?: (id: string) => Promise<boolean>;
25
25
  onPlaceField?: (payload: PlaceFieldPayload) => void;
26
+ /** Optional accessor returning the color to use for the form-field
27
+ * placement preview. Lets the active role color override the default
28
+ * blue without rebuilding the editor between placements. */
29
+ getFormFieldPreviewColor?: () => string | undefined;
26
30
  }
27
31
  export declare class Painter {
28
32
  private userName;
@@ -54,7 +58,10 @@ export declare class Painter {
54
58
  private updateStore;
55
59
  /**
56
60
  * Convert a Konva stage-relative rect to viewport-relative coordinates
57
- * by combining with the stage container's DOM position.
61
+ * by combining with the stage container's DOM position. `getBoundingClientRect`
62
+ * already includes ancestor CSS transforms (panzoom), but `stageRect` is in
63
+ * pre-transform canvas pixels — scale it by the same factor so the offset
64
+ * matches the on-screen size.
58
65
  */
59
66
  private toViewportRect;
60
67
  private parseStampData;
@@ -182,17 +182,21 @@ export class Painter {
182
182
  }
183
183
  /**
184
184
  * Convert a Konva stage-relative rect to viewport-relative coordinates
185
- * by combining with the stage container's DOM position.
185
+ * by combining with the stage container's DOM position. `getBoundingClientRect`
186
+ * already includes ancestor CSS transforms (panzoom), but `stageRect` is in
187
+ * pre-transform canvas pixels — scale it by the same factor so the offset
188
+ * matches the on-screen size.
186
189
  */
187
190
  toViewportRect(pageNumber, stageRect) {
188
191
  const canvas = this.konvaCanvasStore.get(pageNumber);
189
192
  if (!canvas) return stageRect;
190
193
  const containerBounds = canvas.wrapper.getBoundingClientRect();
194
+ const visualScale = canvas.wrapper.offsetWidth > 0 ? containerBounds.width / canvas.wrapper.offsetWidth : 1;
191
195
  return {
192
- x: containerBounds.left + stageRect.x,
193
- y: containerBounds.top + stageRect.y,
194
- width: stageRect.width,
195
- height: stageRect.height
196
+ x: containerBounds.left + stageRect.x * visualScale,
197
+ y: containerBounds.top + stageRect.y * visualScale,
198
+ width: stageRect.width * visualScale,
199
+ height: stageRect.height * visualScale
196
200
  };
197
201
  }
198
202
  parseStampData() {
@@ -332,7 +336,8 @@ export class Painter {
332
336
  editor = new EditorFormField(editorOpts, {
333
337
  fieldType,
334
338
  pageHeight,
335
- onPlaceField
339
+ onPlaceField,
340
+ getPreviewColor: this.callbacks.getFormFieldPreviewColor
336
341
  });
337
342
  break;
338
343
  }
@@ -6,11 +6,17 @@ export interface IEditorFormFieldOptions {
6
6
  /** PDF-space page height, needed to flip Konva page-local Y → PDF Y. */
7
7
  pageHeight: number;
8
8
  onPlaceField: (payload: PlaceFieldPayload) => void;
9
+ /** Resolves to the active role color so the placement preview matches
10
+ * the color the field will be tagged with. Re-read on every drag start
11
+ * so the user can switch roles between placements without rebuilding
12
+ * the editor. */
13
+ getPreviewColor?: () => string | undefined;
9
14
  }
10
15
  export declare class EditorFormField extends Editor {
11
16
  private fieldType;
12
17
  private pageHeight;
13
18
  private onPlaceField;
19
+ private getPreviewColor?;
14
20
  private preview;
15
21
  private vertex;
16
22
  constructor(editorOptions: IEditorOptions, opts: IEditorFormFieldOptions);
@@ -11,6 +11,7 @@ export class EditorFormField extends Editor {
11
11
  fieldType;
12
12
  pageHeight;
13
13
  onPlaceField;
14
+ getPreviewColor;
14
15
  preview = null;
15
16
  vertex = { x: 0, y: 0 };
16
17
  constructor(editorOptions, opts) {
@@ -21,6 +22,7 @@ export class EditorFormField extends Editor {
21
22
  this.fieldType = opts.fieldType;
22
23
  this.pageHeight = opts.pageHeight;
23
24
  this.onPlaceField = opts.onPlaceField;
25
+ this.getPreviewColor = opts.getPreviewColor;
24
26
  }
25
27
  setPageHeight(pageHeight) {
26
28
  this.pageHeight = pageHeight;
@@ -36,7 +38,7 @@ export class EditorFormField extends Editor {
36
38
  y: pos.y,
37
39
  width: 0,
38
40
  height: 0,
39
- stroke: "#1677ff",
41
+ stroke: this.getPreviewColor?.() ?? "#1677ff",
40
42
  strokeWidth: 1,
41
43
  dash: [4, 4],
42
44
  strokeScaleEnabled: false,
@@ -209,6 +209,11 @@ export interface FormFieldDefinition {
209
209
  color?: number[];
210
210
  backgroundColor?: number[];
211
211
  textAlignment?: number;
212
+ /** Opaque role tag for color-coding in form-edit mode. The editor
213
+ * doesn't own role definitions — the host (integrating app) maps
214
+ * roleId → color via the `roleColors` prop on KViewer. Not exported
215
+ * to the PDF. */
216
+ roleId?: string;
212
217
  }
213
218
  export interface FormFieldValue {
214
219
  fieldId: string;
@@ -223,6 +228,65 @@ export interface PlaceFieldPayload {
223
228
  /** PDF-space rect [x1, y1, x2, y2], bottom-left origin. */
224
229
  rectPdf: [number, number, number, number];
225
230
  }
231
+ /**
232
+ * Public payload for adding a form field programmatically. All optional
233
+ * fields fall back to sensible defaults — only `pageNumber`, `fieldType`,
234
+ * and `rect` are required. The created field has `origin: 'placed'` so it
235
+ * is exported into the PDF as a new widget.
236
+ */
237
+ export interface AddFormFieldPayload {
238
+ pageNumber: number;
239
+ fieldType: FormFieldType;
240
+ /** PDF-space rect [x1, y1, x2, y2], bottom-left origin. */
241
+ rect: [number, number, number, number];
242
+ /** Field name. Auto-generated when omitted. Multiple widgets that share
243
+ * a `fieldName` (and `fieldType`) act as one PDF field — values are
244
+ * mirrored across them on edit. Radios with the same `fieldName` form
245
+ * one option group. */
246
+ fieldName?: string;
247
+ /** Opaque role tag for color-coding in form-edit mode. */
248
+ roleId?: string;
249
+ /** Default value for the field. Becomes the initial value too. */
250
+ defaultValue?: string;
251
+ readOnly?: boolean;
252
+ required?: boolean;
253
+ /** checkbox: glyph style. Defaults to 'check'. */
254
+ checkboxStyle?: CheckboxStyle;
255
+ /** signature: placeholder text on the unsigned widget. */
256
+ promptText?: string;
257
+ /** signature: lock other fields once signed. */
258
+ lockAction?: 'all' | 'include' | 'exclude';
259
+ /** signature: field names referenced by lockAction = 'include' / 'exclude'. */
260
+ lockFieldNames?: string[];
261
+ /** dropdown: editable combo box (true) vs list box (false). Defaults to true. */
262
+ combo?: boolean;
263
+ /** dropdown / listbox: allow multiple selections. */
264
+ multiSelect?: boolean;
265
+ /** dropdown / listbox: choices. */
266
+ options?: {
267
+ displayValue: string;
268
+ exportValue: string;
269
+ }[];
270
+ /** text: render as multiple lines. */
271
+ multiLine?: boolean;
272
+ /** text: render as a password field. */
273
+ password?: boolean;
274
+ /** text: render as evenly-spaced character cells (requires `maxLen`). */
275
+ comb?: boolean;
276
+ /** text: maximum input length. */
277
+ maxLen?: number;
278
+ /** radio: this widget's option value within its group. Auto-generated
279
+ * when omitted (e.g. "Option_1", "Option_2", …). */
280
+ buttonValue?: string;
281
+ fontSize?: number;
282
+ fontName?: string;
283
+ /** RGB color tuple in 0–1 (PDF native). */
284
+ color?: number[];
285
+ /** RGB background color tuple in 0–1 (PDF native). */
286
+ backgroundColor?: number[];
287
+ /** 0 = left, 1 = center, 2 = right. */
288
+ textAlignment?: number;
289
+ }
226
290
  export interface DetectedShapeRect {
227
291
  x: number;
228
292
  y: number;
@@ -1,4 +1,14 @@
1
- import { PDFBool, PDFDict, PDFName, PDFString, StandardFonts, rgb } from "pdf-lib";
1
+ import {
2
+ AcroFieldFlags,
3
+ AnnotationFlags,
4
+ PDFAcroSignature,
5
+ PDFBool,
6
+ PDFDict,
7
+ PDFName,
8
+ PDFRawStream,
9
+ PDFString,
10
+ StandardFonts
11
+ } from "pdf-lib";
2
12
  import { CHECKBOX_STYLE_TABLE } from "../checkbox-styles.js";
3
13
  export async function writeFormFieldsToPdf(pdfDoc, fieldValues, placedDefinitions = []) {
4
14
  const valueByFieldId = new Map(fieldValues.map((v) => [v.fieldId, v]));
@@ -206,13 +216,7 @@ async function createWidgetForDefinition(pdfDoc, def, fv, radioGroupCache) {
206
216
  break;
207
217
  }
208
218
  case "signature": {
209
- const v = fv?.value;
210
- const signed = typeof v === "string" && v.startsWith("data:image");
211
- if (signed) {
212
- await drawSignatureImage(pdfDoc, page, v, box);
213
- } else {
214
- await drawSignaturePlaceholder(pdfDoc, page, box);
215
- }
219
+ await createSignatureField(pdfDoc, page, def, fv);
216
220
  break;
217
221
  }
218
222
  case "dropdown": {
@@ -244,55 +248,101 @@ function applyFlags(field, def) {
244
248
  if (def.readOnly) field.enableReadOnly();
245
249
  if (def.required) field.enableRequired();
246
250
  }
247
- async function drawSignatureImage(pdfDoc, page, dataUrl, box) {
251
+ async function createSignatureField(pdfDoc, page, def, fv) {
252
+ const { context } = pdfDoc;
253
+ const form = pdfDoc.getForm();
254
+ const [x1, y1, x2, y2] = def.rect;
255
+ const width = Math.max(1, x2 - x1);
256
+ const height = Math.max(1, y2 - y1);
257
+ const fieldDict = context.obj({
258
+ FT: "Sig",
259
+ Kids: []
260
+ });
261
+ const fieldRef = context.register(fieldDict);
262
+ const sig = PDFAcroSignature.fromDict(fieldDict, fieldRef);
263
+ sig.setPartialName(def.fieldName);
264
+ if (def.readOnly) sig.setFlagTo(AcroFieldFlags.ReadOnly, true);
265
+ if (def.required) sig.setFlagTo(AcroFieldFlags.Required, true);
266
+ const v = fv?.value;
267
+ const signed = typeof v === "string" && v.startsWith("data:image");
268
+ const appearanceRef = signed ? await buildSignedSignatureAppearance(pdfDoc, v, width, height) : await buildSignaturePlaceholderAppearance(pdfDoc, width, height);
269
+ const widgetDict = context.obj({
270
+ Type: "Annot",
271
+ Subtype: "Widget",
272
+ Rect: [x1, y1, x2, y2],
273
+ P: page.ref,
274
+ Parent: fieldRef,
275
+ F: 1 << AnnotationFlags.Print,
276
+ AP: { N: appearanceRef }
277
+ });
278
+ const widgetRef = context.register(widgetDict);
279
+ sig.addWidget(widgetRef);
280
+ form.acroForm.addField(fieldRef);
281
+ page.node.addAnnot(widgetRef);
282
+ }
283
+ async function buildSignaturePlaceholderAppearance(pdfDoc, width, height) {
284
+ const { context } = pdfDoc;
285
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
286
+ const label = "Sign here";
287
+ const fontSize = Math.max(6, Math.min(12, height * 0.4));
288
+ const textWidth = font.widthOfTextAtSize(label, fontSize);
289
+ const textHeight = font.heightAtSize(fontSize);
290
+ const textX = (width - textWidth) / 2;
291
+ const textY = (height - textHeight) / 2;
292
+ const ops = [
293
+ "q",
294
+ "0.55 0.55 0.55 RG",
295
+ "0.75 w",
296
+ "[3 2] 0 d",
297
+ `0.5 0.5 ${(width - 1).toFixed(3)} ${(height - 1).toFixed(3)} re`,
298
+ "S",
299
+ "0.55 0.55 0.55 rg",
300
+ "BT",
301
+ `/F1 ${fontSize.toFixed(3)} Tf`,
302
+ `${textX.toFixed(3)} ${textY.toFixed(3)} Td`,
303
+ `(${label}) Tj`,
304
+ "ET",
305
+ "Q"
306
+ ].join("\n");
307
+ const dict = context.obj({
308
+ Type: "XObject",
309
+ Subtype: "Form",
310
+ BBox: [0, 0, width, height],
311
+ Matrix: [1, 0, 0, 1, 0, 0],
312
+ Resources: { Font: { F1: font.ref } }
313
+ });
314
+ const stream = PDFRawStream.of(dict, new TextEncoder().encode(ops));
315
+ return context.register(stream);
316
+ }
317
+ async function buildSignedSignatureAppearance(pdfDoc, dataUrl, width, height) {
248
318
  const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
249
- if (!match) return;
319
+ if (!match) return buildSignaturePlaceholderAppearance(pdfDoc, width, height);
250
320
  const mimeType = match[1] ?? "";
251
321
  const base64 = match[2] ?? "";
252
- let image;
253
- if (mimeType.includes("jpeg") || mimeType.includes("jpg")) {
254
- image = await pdfDoc.embedJpg(base64);
255
- } else {
256
- image = await pdfDoc.embedPng(base64);
257
- }
322
+ const image = mimeType.includes("jpeg") || mimeType.includes("jpg") ? await pdfDoc.embedJpg(base64) : await pdfDoc.embedPng(base64);
258
323
  const imgAspect = image.width / image.height;
259
- const boxAspect = box.width / box.height;
260
- let drawW = box.width;
261
- let drawH = box.height;
262
- if (imgAspect > boxAspect) {
263
- drawH = box.width / imgAspect;
264
- } else {
265
- drawW = box.height * imgAspect;
266
- }
267
- const drawX = box.x + (box.width - drawW) / 2;
268
- const drawY = box.y + (box.height - drawH) / 2;
269
- page.drawImage(image, { x: drawX, y: drawY, width: drawW, height: drawH });
270
- }
271
- async function drawSignaturePlaceholder(pdfDoc, page, box) {
272
- page.drawRectangle({
273
- x: box.x,
274
- y: box.y,
275
- width: box.width,
276
- height: box.height,
277
- borderColor: rgb(0.55, 0.55, 0.55),
278
- borderWidth: 0.75,
279
- borderDashArray: [3, 2]
324
+ const boxAspect = width / height;
325
+ let drawW = width;
326
+ let drawH = height;
327
+ if (imgAspect > boxAspect) drawH = width / imgAspect;
328
+ else drawW = height * imgAspect;
329
+ const drawX = (width - drawW) / 2;
330
+ const drawY = (height - drawH) / 2;
331
+ const ops = [
332
+ "q",
333
+ `${drawW.toFixed(3)} 0 0 ${drawH.toFixed(3)} ${drawX.toFixed(3)} ${drawY.toFixed(3)} cm`,
334
+ "/Img Do",
335
+ "Q"
336
+ ].join("\n");
337
+ const dict = pdfDoc.context.obj({
338
+ Type: "XObject",
339
+ Subtype: "Form",
340
+ BBox: [0, 0, width, height],
341
+ Matrix: [1, 0, 0, 1, 0, 0],
342
+ Resources: { XObject: { Img: image.ref } }
280
343
  });
281
- try {
282
- const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
283
- const label = "Sign here";
284
- const fontSize = Math.max(6, Math.min(12, box.height * 0.4));
285
- const textWidth = font.widthOfTextAtSize(label, fontSize);
286
- const textHeight = font.heightAtSize(fontSize);
287
- page.drawText(label, {
288
- x: box.x + (box.width - textWidth) / 2,
289
- y: box.y + (box.height - textHeight) / 2,
290
- size: fontSize,
291
- font,
292
- color: rgb(0.55, 0.55, 0.55)
293
- });
294
- } catch {
295
- }
344
+ const stream = PDFRawStream.of(dict, new TextEncoder().encode(ops));
345
+ return pdfDoc.context.register(stream);
296
346
  }
297
347
  async function embedSignatureImage(pdfDoc, field) {
298
348
  const dataUrl = field.value;
@@ -1 +1 @@
1
- .kviewer-text-layer{left:0;line-height:1;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:1}.kviewer-text-layer--interactive{cursor:text;-webkit-user-select:text;-moz-user-select:text;user-select:text}.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer *{cursor:text!important}.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_selector_hover{cursor:pointer!important}.kviewer-form-layer{left:0;overflow:hidden;position:absolute;top:0}.kviewer-form-field{box-sizing:border-box;position:absolute}.kviewer-form-input{background-color:rgba(224,232,255,.6);border:1px solid transparent;box-sizing:border-box;color:#000;display:block;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;height:100%;line-height:normal;margin:0;outline:none;padding:1px 2px;resize:none;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;vertical-align:top}.kviewer-form-input:hover{background-color:rgba(224,232,255,.8);border-color:rgba(0,0,0,.2)}.kviewer-form-input:focus{background-color:rgba(224,232,255,.9);border-color:var(--color-primary,#3b82f6);outline:2px solid var(--color-primary,#3b82f6)}.kviewer-form-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;cursor:pointer}.kviewer-form-listbox{overflow-y:auto;padding:0}.kviewer-form-listbox option{padding:1px 3px}.kviewer-form-editable-combo{height:100%;position:relative;width:100%}.kviewer-form-editable-combo input{height:100%;width:100%}.kviewer-form-checkbox,.kviewer-form-radio{align-items:center;background:#fff;box-sizing:border-box;cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-checkbox input,.kviewer-form-radio input{accent-color:var(--color-primary,#3b82f6);cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-checkbox--detected{background:transparent}.kviewer-form-checkbox--circle{border-radius:50%}.kviewer-form-checkbox__icon{color:#1a1a1a;height:65%;width:65%}.kviewer-form-checkbox--circle .kviewer-form-checkbox__icon{height:50%;width:50%}.kviewer-form-radio input{accent-color:var(--color-primary,#3b82f6);cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-button{align-items:center;background:#c8c8c8;border:1px solid rgba(0,0,0,.35);box-sizing:border-box;color:#000;cursor:pointer;display:flex;font-family:inherit;font-weight:700;height:100%;justify-content:center;line-height:1;padding:0 6px;text-align:center;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.kviewer-form-button:hover{background:#bdbdbd}.kviewer-form-button:active{background:#b0b0b0}.kviewer-form-signature{align-items:center;background:rgba(255,255,200,.15);border:1px dashed rgba(0,0,0,.2);cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-signature:hover{background:rgba(59,130,246,.08);border-color:var(--color-primary,#3b82f6)}.kviewer-form-signature--filled{background:transparent;border-color:transparent;border-style:solid}.kviewer-form-signature__preview{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain}.kviewer-form-signature__placeholder{color:rgba(0,0,0,.4);font-size:10px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}
1
+ .kviewer-text-layer{left:0;line-height:1;overflow:hidden;position:absolute;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:1}.kviewer-text-layer--interactive{cursor:text;-webkit-user-select:text;-moz-user-select:text;user-select:text}.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_12 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_13 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_5 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_6 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_7 .kviewer-annotation-layer *,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_8 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_4 .kviewer-annotation-layer *{cursor:text!important}.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer,.KViewer_is_painting.KViewer_painting_type_11 .kviewer-annotation-layer *{cursor:crosshair!important}.KViewer_selector_hover{cursor:pointer!important}.kviewer-form-layer{left:0;overflow:hidden;position:absolute;top:0}.kviewer-form-field{box-sizing:border-box;position:absolute}.kviewer-form-input{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 14%,transparent);border:1px solid transparent;box-sizing:border-box;color:#000;display:block;font-family:-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;height:100%;line-height:normal;margin:0;outline:none;padding:1px 2px;resize:none;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;vertical-align:top}.kviewer-form-input:hover{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 22%,transparent);border-color:rgba(0,0,0,.2)}.kviewer-form-input:focus{background-color:color-mix(in srgb,var(--kvw-field-color,#1677ff) 28%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6));outline:2px solid var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto;cursor:pointer}.kviewer-form-listbox{overflow-y:auto;padding:0}.kviewer-form-listbox option{padding:1px 3px}.kviewer-form-editable-combo{height:100%;position:relative;width:100%}.kviewer-form-editable-combo input{height:100%;width:100%}.kviewer-form-checkbox,.kviewer-form-radio{align-items:center;background:#fff;box-sizing:border-box;cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-checkbox input,.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-checkbox--detected{background:transparent}.kviewer-form-checkbox--circle{border-radius:50%}.kviewer-form-checkbox__icon{color:#1a1a1a;height:65%;width:65%}.kviewer-form-checkbox--circle .kviewer-form-checkbox__icon{height:50%;width:50%}.kviewer-form-radio input{accent-color:var(--kvw-field-color,var(--color-primary,#3b82f6));cursor:pointer;height:80%;margin:0;max-height:18px;max-width:18px;width:80%}.kviewer-form-button{align-items:center;background:#c8c8c8;border:1px solid rgba(0,0,0,.35);box-sizing:border-box;color:#000;cursor:pointer;display:flex;font-family:inherit;font-weight:700;height:100%;justify-content:center;line-height:1;padding:0 6px;text-align:center;width:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.kviewer-form-button:hover{background:#bdbdbd}.kviewer-form-button:active{background:#b0b0b0}.kviewer-form-signature{align-items:center;background:rgba(255,255,200,.15);border:1px dashed rgba(0,0,0,.2);cursor:pointer;display:flex;height:100%;justify-content:center;width:100%}.kviewer-form-signature:hover{background:color-mix(in srgb,var(--kvw-field-color,#3b82f6) 8%,transparent);border-color:var(--kvw-field-color,var(--color-primary,#3b82f6))}.kviewer-form-signature--filled{background:transparent;border-color:transparent;border-style:solid}.kviewer-form-signature__preview{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain}.kviewer-form-signature__placeholder{color:rgba(0,0,0,.4);font-size:10px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}
@@ -54,6 +54,7 @@
54
54
 
55
55
  <script setup>
56
56
  import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from "vue";
57
+ import { useRuntimeConfig } from "#imports";
57
58
  import { useViewerState } from "../composables/useViewerState";
58
59
  import { useViewerSearch } from "../composables/useViewerSearch";
59
60
  import { usePageProxyCache } from "../composables/usePageProxyCache";
@@ -71,6 +72,10 @@ const props = defineProps({
71
72
  const canvasRef = ref(null);
72
73
  const textLayerRef = ref(null);
73
74
  const annotationRef = ref(null);
75
+ const runtimeConfig = useRuntimeConfig();
76
+ const minRenderPixelRatio = Number(
77
+ runtimeConfig.public.kviewer?.minRenderPixelRatio ?? 2
78
+ );
74
79
  const state = useViewerState();
75
80
  const search = useViewerSearch();
76
81
  const proxyCache = usePageProxyCache();
@@ -181,7 +186,7 @@ async function renderCanvas(opts) {
181
186
  currentRenderTask.cancel();
182
187
  currentRenderTask = null;
183
188
  }
184
- const dpr = window.devicePixelRatio || 1;
189
+ const dpr = Math.max(window.devicePixelRatio || 1, minRenderPixelRatio);
185
190
  const quality = props.renderQuality ?? 1;
186
191
  const viewport = proxy.getViewport({
187
192
  scale: props.scale * quality * dpr
@@ -1,6 +1,7 @@
1
1
  import { type ViewMode } from '../composables/useViewerState.js';
2
- import type { IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
2
+ import type { AddFormFieldPayload, FormFieldDefinition, IAnnotationStore, StampDefinition, SignatureHandlers } from '../annotation/engine/types.js';
3
3
  import { type ExportPdfOptions } from '../annotation/pdf-export/export.js';
4
+ import type { ViewerMenuItem } from '../menu-items.js';
4
5
  type __VLS_Props = {
5
6
  source: string | Uint8Array | object;
6
7
  textLayer?: boolean;
@@ -21,6 +22,29 @@ type __VLS_Props = {
21
22
  * with selection chrome, can be moved/resized, and the property panel
22
23
  * is shown. Supports v-model via `v-model:form-edit-mode`. */
23
24
  formEditMode?: boolean;
25
+ /** Map of roleId → CSS color used to color-code form fields in
26
+ * form-edit mode. The editor stays unopinionated about role
27
+ * definitions: the host owns the role list, names, and color picker,
28
+ * and just hands a flat lookup table to the viewer. Unknown or
29
+ * missing ids fall back to the default blue. */
30
+ roleColors?: Record<string, string>;
31
+ /** Active role id new field placements are auto-tagged with. The host
32
+ * drives this — typically by binding it to a role picker in its own
33
+ * UI. Supports v-model via `v-model:active-role-id`. */
34
+ activeRoleId?: string | null;
35
+ /** Execute embedded JavaScript inside the PDF — field-level AA scripts
36
+ * (Mouse Up, Calculate, Format, Validate, Keystroke), document-level
37
+ * open actions, and page-level scripts. Powered by pdf.js's QuickJS
38
+ * WASM sandbox (no DOM/network/host-JS escape), but arbitrary JS from
39
+ * untrusted PDFs is still real attack surface — default is `false`.
40
+ * Treated as read-once per document; toggling at runtime requires
41
+ * changing the `source` prop or remounting the component. */
42
+ scripting?: boolean;
43
+ /** Extra items appended to the default burger menu (after Download
44
+ * and the form-field-detection toggle). Use this to expose app-level
45
+ * toggles or actions without replacing the entire header. Ignored
46
+ * when the host supplies a custom `#header` slot. */
47
+ menuItems?: ViewerMenuItem[];
24
48
  };
25
49
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
26
50
  type ImportMode = 'replace' | 'merge';
@@ -44,6 +68,16 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
44
68
  getKonvaCanvasState: typeof getKonvaCanvasState;
45
69
  getFormFieldValues: () => import("../annotation/engine/types.js").FormFieldValue[];
46
70
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
71
+ /** Add a form field programmatically. Returns the created definition.
72
+ * The new field is exported into the PDF on `exportPdf()` like any
73
+ * field placed via the placement tool. */
74
+ addFormField: (payload: AddFormFieldPayload) => FormFieldDefinition;
75
+ /** Patch a form field by id (rect, name, flags, type-specific props). */
76
+ updateFormField: (id: string, patch: Partial<FormFieldDefinition>) => void;
77
+ /** Remove a form field by id (drops its value too). */
78
+ removeFormField: (id: string) => void;
79
+ /** All form-field definitions (parsed, detected, and placed), flat. */
80
+ getFormFields: () => FormFieldDefinition[];
47
81
  /** Reactive form-edit-mode flag — read with `.value`. */
48
82
  formEditMode: import("vue").Ref<boolean, boolean>;
49
83
  /** Programmatically enter or leave form-edit mode. */
@@ -52,19 +86,25 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
52
86
  toggleFormEditMode: () => boolean;
53
87
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
54
88
  "update:formEditMode": (value: boolean) => any;
89
+ "update:activeRoleId": (value: string | null) => any;
55
90
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
56
91
  "onUpdate:formEditMode"?: ((value: boolean) => any) | undefined;
92
+ "onUpdate:activeRoleId"?: ((value: string | null) => any) | undefined;
57
93
  }>, {
58
94
  userName: string;
59
95
  freehandGroupingDelay: number;
60
96
  zoom: number;
61
97
  readonly: boolean;
62
98
  active: boolean;
99
+ menuItems: ViewerMenuItem[];
100
+ scripting: boolean;
63
101
  stamps: StampDefinition[];
64
102
  signatureHandlers: SignatureHandlers;
65
103
  viewMode: ViewMode;
66
104
  shapeDetection: boolean;
67
105
  formEditMode: boolean;
106
+ roleColors: Record<string, string>;
107
+ activeRoleId: string | null;
68
108
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
69
109
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
70
110
  declare const _default: typeof __VLS_export;