import PSPDFKitType, { Action, AnnotationsUnion, SignatureFormField as SignatureFormFieldType } from "./index"; import { Instance, WidgetAnnotation, ToolbarItem } from "./index"; import { EnvelopeResponse, Envelope, User, Element, Document, IFunction } from "./interfaces"; declare const PSPDFKit: typeof PSPDFKitType const { List } = PSPDFKit.Immutable; const { Rect } = PSPDFKit.Geometry; const { SignatureFormField } = PSPDFKit.FormFields; const { DRAW, TYPE } = PSPDFKit.ElectronicSignatureCreationMode; const { DISABLED } = PSPDFKit.AutoSaveMode; enum ActionType { Created = 0, Saved = 1, Sent = 2, EmailSent = 3, Delivered = 4, Seen = 5, Signed = 6, Rejected = 7, } export class App { public static Instance: Instance; public static currentDocument: Document; public static envelopeKey: string; public static UI: UI; public static Network: Network; public static Annotation: Annotation; // This function will be called in the ShowEnvelope.razor page // and will trigger loading of the Editor Interface public static async init(container: string, envelopeKey: string) { // Initialize classes console.debug("Initializing classes..") App.UI = new UI(); App.Network = new Network(); App.Annotation = new Annotation(); // Load the envelope from the database console.debug("Loading envelope from database..") const envelopeObject: EnvelopeResponse = await App.Network.getEnvelope(envelopeKey); console.debug(envelopeObject) App.envelopeKey = envelopeKey; App.currentDocument = envelopeObject.envelope.documents[0]; // Load the document from the filestore console.debug("Loading document from filestore") let arrayBuffer try { arrayBuffer = await App.Network.getDocument(envelopeKey, App.currentDocument.id); } catch (e) { console.error(e) } // Load PSPDFKit console.debug("Loading PSPDFKit..") App.Instance = await App.UI.loadPSPDFKit(arrayBuffer, container) App.UI.configurePSPDFKit(this.Instance, App.handleClick) // Load annotations into PSPDFKit console.debug("Loading annotations..") const annotations = App.Annotation.createAnnotations(App.currentDocument) const createdAnnotations = await App.Instance.create(annotations) const description = "Umschlag wurde geöffnet" await App.Network.postHistory(App.envelopeKey, ActionType.Seen, description); } public static async handleClick(eventType: string) { let result = false; switch (eventType) { case "RESET": result = await App.handleReset(null) if (result == true) { alert("Dokument zurückgesetzt!"); } else { alert("Fehler beim Zurücksetzen des Dokuments!") } break; case "FINISH": result = await App.handleFinish(null) if (result == true) { // TODO: Redirect to success page alert("Dokument erfolgreich signiert!") } else { alert("Fehler beim Abschließen des Dokuments!") } break; } } public static async handleFinish(event: any): Promise { // Save changes before doing anything try { await App.Instance.save(); } catch (e) { console.error(e); return false; } // Export annotation data and save to database try { const json = await App.Instance.exportInstantJSON() const postEnvelopeResult: boolean = await App.Network.postEnvelope(App.envelopeKey, App.currentDocument.id, JSON.stringify(json)) if (postEnvelopeResult === false) { return false; } } catch (e) { console.error(e); return false; } // Flatten the annotations and save the document to disk try { const buffer = await App.Instance.exportPDF({ flatten: true }); const postDocumentResult: boolean = await App.Network.postDocument(App.envelopeKey, App.currentDocument.id, buffer); if (postDocumentResult === false) { return false; } } catch (e) { console.error(e); return false; } try { const description = "Dokument wurde signiert" await App.Network.postHistory(App.envelopeKey, ActionType.Signed, description); } catch (e) { console.error(e); return false; } return true; } public static async handleReset(event: any): Promise { if (confirm("Wollen Sie das Dokument und alle erstellten Signaturen zurücksetzen?")) { const result = App.Annotation.deleteAnnotations(App.Instance) return true; } else { return true; } } private static async downloadDocument() { const buffer = await App.Instance.exportPDF({ flatten: true }); const supportsDownloadAttribute = HTMLAnchorElement.prototype.hasOwnProperty("download"); const blob = new Blob([buffer], { type: "application/pdf" }); if (!supportsDownloadAttribute) { const reader = new FileReader(); reader.onloadend = function () { const dataUrl = reader.result; downloadPdf(dataUrl); }; reader.readAsDataURL(blob); } else { const objectUrl = window.URL.createObjectURL(blob); downloadPdf(objectUrl); window.URL.revokeObjectURL(objectUrl); } function downloadPdf(blob) { const a = document.createElement("a"); a.href = blob; a.style.display = "none"; a.download = "download.pdf"; a.setAttribute("download", "download.pdf"); document.body.appendChild(a); a.click(); document.body.removeChild(a); } } } class Annotation { public createAnnotations(document: Document): AnnotationsUnion[] { const annotations: any[] = []; document.elements.forEach((element: Element) => { console.log("Creating annotation for element", element.id) const [annotation, formField] = this.createAnnotationFromElement(element) annotations.push(annotation); annotations.push(formField); }) return annotations; } public async deleteAnnotations(instance: Instance): Promise { let pageAnnotations = ( await Promise.all(Array.from({ length: instance.totalPageCount }).map((_, pageIndex) => instance.getAnnotations(pageIndex) )) ).flatMap((annotations) => annotations.reduce((acc, annotation) => acc.concat(annotation), []) ).filter((annotation) => !!annotation.isSignature); //deleting all Annotations return await instance.delete(pageAnnotations); } private createAnnotationFromElement(element: Element): [annotation: WidgetAnnotation, formField: SignatureFormFieldType] { const id = PSPDFKit.generateInstantId() const width = this.inchToPoint(element.width) const height = this.inchToPoint(element.height) const top = this.inchToPoint(element.top) - (height / 2) const left = this.inchToPoint(element.left) - (width / 2) const page = element.page - 1 const annotation: WidgetAnnotation = this.createSignatureAnnotation(id, width, height, top, left, page) console.log(annotation) const formField = new SignatureFormField({ name: id, annotationIds: List([annotation.id]) }) console.log(formField) return [annotation, formField] } private createSignatureAnnotation(id: string, width: number, height: number, top: number, left: number, pageIndex: number): WidgetAnnotation { const annotation = new PSPDFKit.Annotations.WidgetAnnotation({ id: id, pageIndex: pageIndex, formFieldName: id, boundingBox: new Rect({ width, height, top, left }) }) return annotation } private inchToPoint(inch: number): number { return inch * 72; } } class Network { public getEnvelope(envelopeKey: string): Promise { return fetch(`/api/envelope/${envelopeKey}`, this.withCSRFToken({ credentials: "include" })) .then(res => res.json()); } public getDocument(envelopeKey: string, documentId: number): Promise { return fetch(`/api/document/${envelopeKey}?index=${documentId}`, this.withCSRFToken({ credentials: "include" })) .then(res => res.arrayBuffer()); } public postDocument(envelopeKey: string, documentId: number, buffer: ArrayBuffer): Promise { const url = `/api/document/${envelopeKey}?index=${documentId}`; const options: RequestInit = { credentials: "include", method: "POST", body: buffer } console.debug("PostDocument/Calling url: " + url) return fetch(url, this.withCSRFToken(options)) .then(this.handleResponse) .then((res: Response) => { if (!res.ok) { return false; }; return true; }); } public postEnvelope(envelopeKey: string, documentId: number, jsonString: string): Promise { const url = `/api/envelope/${envelopeKey}?index=${documentId}`; const options: RequestInit = { credentials: "include", method: "POST", body: jsonString } console.debug("PostEnvelope/Calling url: " + url) return fetch(url, this.withCSRFToken(options)) .then(this.handleResponse) .then((res: Response) => { if (!res.ok) { return false; }; return true; }); } public postHistory(envelopeKey: string, actionType: ActionType, actionDescription: string): Promise { const url = `/api/history/${envelopeKey}`; const data = { actionDescription: actionDescription, actionType: actionType.toString() } const options: RequestInit = { credentials: "include", method: "POST", headers: { 'Content-Type': "application/json; charset=utf-8" }, body: JSON.stringify(data) } console.debug("PostHistory/Calling url: " + url) return fetch(url, this.withCSRFToken(options)) .then(this.handleResponse) .then((res: Response) => { if (!res.ok) { return false; }; return true; }); } private withCSRFToken(options: RequestInit): RequestInit { const token = (document.getElementsByName("__RequestVerificationToken")[0] as any).value; let headers = options.headers; options.headers = { ...headers, 'X-XSRF-TOKEN': token }; return options; } private handleResponse(res: Response) { if (!res.ok) { console.log(`Request failed with status ${res.status}`) return res } else { return res } } } class UI { public allowedToolbarItems: string[] = [ "sidebar-thumbnails", "sidebar-document-ouline", "sidebar-bookmarks", "pager", "pan", "zoom-out", "zoom-in", "zoom-mode", "spacer", "search" ] // Load the PSPDFKit UI by setting a target element as the container to render in // and a arraybuffer which represents the document that should be displayed. public loadPSPDFKit(arrayBuffer: ArrayBuffer, container: string): Promise { return PSPDFKit.load({ container: container, document: arrayBuffer, autoSaveMode: DISABLED, annotationPresets: this.getPresets(), electronicSignatures: { creationModes: [DRAW, TYPE] }, isEditableAnnotation: function (annotation: WidgetAnnotation) { // Check if the annotation is a signature // This will allow new signatures, but not allow edits. return !annotation.isSignature; } }) } public configurePSPDFKit(instance: Instance, handler: any) { instance.addEventListener("annotations.load", (loadedAnnotations) => { console.log("annotations loaded", loadedAnnotations.toJS()); }) instance.addEventListener("annotations.change", () => { console.log("annotations changed") }) instance.addEventListener("annotations.create", async (createdAnnotations) => { console.log("annotations created"); }) const toolbarItems = this.getToolbarItems(instance, handler) instance.setToolbarItems(toolbarItems) console.debug("PSPDFKit configured!"); } public getToolbarItems(instance: Instance, handler: any): ToolbarItem[] { const customItems = this.getCustomItems(handler) const defaultItems: Array = this.getDefaultItems(instance.toolbarItems) return defaultItems.concat(customItems) } private getCustomItems = function (callback: any) { const customItems: ToolbarItem[] = [ { type: "custom", id: "button-reset", className: "button-reset", title: "Zurücksetzen", onPress() { callback("RESET") }, icon: ` ` }, { type: "custom", id: "button-finish", className: "button-finish", title: "Abschließen", onPress() { callback("FINISH") }, icon: ` ` } ] return customItems } private getDefaultItems(items: ToolbarItem[]): ToolbarItem[] { return items.filter((item) => this.allowedToolbarItems.includes(item.type)) } private getPresets() { const annotationPresets = PSPDFKit.defaultAnnotationPresets; annotationPresets.ink = { lineWidth: 10 }; annotationPresets.widget = { readOnly: true } return annotationPresets; } }