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.
- package/dist/module.d.mts +9 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +22 -2
- package/dist/runtime/annotation/engine/painter.d.ts +8 -1
- package/dist/runtime/annotation/engine/painter.js +11 -6
- package/dist/runtime/annotation/engine/tools/form-field.d.ts +6 -0
- package/dist/runtime/annotation/engine/tools/form-field.js +3 -1
- package/dist/runtime/annotation/engine/types.d.ts +64 -0
- package/dist/runtime/annotation/pdf-export/export-form-fields.js +102 -52
- package/dist/runtime/assets/kviewer.css +1 -1
- package/dist/runtime/components/PdfPage.vue +6 -1
- package/dist/runtime/components/Viewer.d.vue.ts +41 -1
- package/dist/runtime/components/Viewer.vue +84 -3
- package/dist/runtime/components/Viewer.vue.d.ts +41 -1
- package/dist/runtime/components/ViewerBar.d.vue.ts +7 -1
- package/dist/runtime/components/ViewerBar.vue +36 -0
- package/dist/runtime/components/ViewerBar.vue.d.ts +7 -1
- package/dist/runtime/components/ViewerTabs.d.vue.ts +29 -6
- package/dist/runtime/components/ViewerTabs.vue +11 -2
- package/dist/runtime/components/ViewerTabs.vue.d.ts +29 -6
- package/dist/runtime/components/form-fields/FormFieldWrapper.vue +10 -2
- package/dist/runtime/components/form-fields/PlacedFieldChrome.vue +1 -1
- package/dist/runtime/composables/useAnnotationEngine.d.ts +1 -0
- package/dist/runtime/composables/useAnnotationEngine.js +2 -1
- package/dist/runtime/composables/useFormFields.d.ts +40 -3
- package/dist/runtime/composables/useFormFields.js +101 -1
- package/dist/runtime/composables/useInertiaPanzoom.js +1 -0
- package/dist/runtime/composables/usePageProxyCache.d.ts +4 -0
- package/dist/runtime/composables/usePageProxyCache.js +4 -1
- package/dist/runtime/composables/useScriptingBridge.d.ts +29 -0
- package/dist/runtime/composables/useScriptingBridge.js +74 -0
- package/dist/runtime/composables/useScriptingManager.d.ts +65 -0
- package/dist/runtime/composables/useScriptingManager.js +123 -0
- package/dist/runtime/embed/bridge-client.d.ts +58 -0
- package/dist/runtime/embed/bridge-client.js +143 -0
- package/dist/runtime/embed/bridge-host.d.ts +59 -0
- package/dist/runtime/embed/bridge-host.js +136 -0
- package/dist/runtime/embed/protocol.d.ts +105 -0
- package/dist/runtime/embed/protocol.js +13 -0
- package/dist/runtime/menu-items.d.ts +34 -0
- package/dist/runtime/menu-items.js +0 -0
- package/dist/runtime/public-types.d.ts +8 -1
- package/dist/runtime/public-types.js +3 -0
- package/dist/types.d.mts +1 -1
- 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
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
260
|
-
let drawW =
|
|
261
|
-
let drawH =
|
|
262
|
-
if (imgAspect > boxAspect)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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:
|
|
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;
|