@dbp-topics/signature
Version:
[GitLab Repository](https://gitlab.tugraz.at/dbp/esign/signature) | [npmjs package](https://www.npmjs.com/package/@dbp-topics/signature) | [Unpkg CDN](https://unpkg.com/browse/@dbp-topics/signature/) | [Esign Bundle](https://gitlab.tugraz.at/dbp/esign/dbp
750 lines (662 loc) • 29.5 kB
JavaScript
import {createInstance} from './i18n.js';
import {css, html} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
import {live} from 'lit/directives/live.js';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {MiniSpinner, Icon} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import pdfjs from 'pdfjs-dist/legacy/build/pdf.js';
import {name as pkgName} from './../package.json';
import {readBinaryFileContent} from './utils.js';
/**
* PdfPreview web component
*/
export class PdfPreview extends ScopedElementsMixin(DBPLitElement) {
constructor() {
super();
this._i18n = createInstance();
this.lang = this._i18n.language;
this.pdfDoc = null;
this.currentPage = 0;
this.totalPages = 0;
this.isShowPage = false;
this.isPageLoaded = false;
this.showErrorMessage = false;
this.isPageRenderingInProgress = false;
this.isShowPlacement = true;
this.canvas = null;
this.annotationLayer = null;
this.fabricCanvas = null;
this.canvasToPdfScale = 1.0;
this.currentPageOriginalHeight = 0;
this.placeholder = '';
this.signature_width = 42;
this.signature_height = 42;
this.border_width = 2;
this.allowSignatureRotation = false;
this._onWindowResize = this._onWindowResize.bind(this);
}
static get scopedElements() {
return {
'dbp-mini-spinner': MiniSpinner,
'dbp-icon': Icon,
};
}
/**
* See: https://lit-element.polymer-project.org/guide/properties#initialize
*/
static get properties() {
return {
...super.properties,
lang: {type: String},
currentPage: {type: Number, attribute: false},
totalPages: {type: Number, attribute: false},
isShowPage: {type: Boolean, attribute: false},
isPageRenderingInProgress: {type: Boolean, attribute: false},
isPageLoaded: {type: Boolean, attribute: false},
showErrorMessage: {type: Boolean, attribute: false},
isShowPlacement: {type: Boolean, attribute: false},
placeholder: {type: String, attribute: 'signature-placeholder-image-src'},
signature_width: {type: Number, attribute: 'signature-width'},
signature_height: {type: Number, attribute: 'signature-height'},
allowSignatureRotation: {type: Boolean, attribute: 'allow-signature-rotation'},
};
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
switch (propName) {
case 'lang':
this._i18n.changeLanguage(this.lang);
break;
}
});
super.update(changedProperties);
}
_onWindowResize() {
this.showPage(this.currentPage);
}
disconnectedCallback() {
if (this.fabricCanvas !== null) {
this.fabricCanvas.removeListeners();
this.fabricCanvas = null;
}
window.removeEventListener('resize', this._onWindowResize);
super.disconnectedCallback();
}
connectedCallback() {
super.connectedCallback();
const that = this;
pdfjs.GlobalWorkerOptions.workerSrc = commonUtils.getAssetURL(
pkgName,
'pdfjs/pdf.worker.js'
);
window.addEventListener('resize', this._onWindowResize);
this.updateComplete.then(async () => {
that.annotationLayer = that._('#annotation-layer');
const fabric = (await import('fabric')).fabric;
that.canvas = that._('#pdf-canvas');
// this._('#upload-pdf-input').addEventListener('change', function() {
// that.showPDF(this.files[0]);
// });
// add fabric.js canvas for signature positioning
// , {stateful : true}
// selection is turned off because it makes troubles on mobile devices
this.fabricCanvas = new fabric.Canvas(this._('#fabric-canvas'), {
selection: false,
allowTouchScrolling: true,
});
// add signature image
fabric.Image.fromURL(this.placeholder, function (image) {
// add a red border around the signature placeholder
image.set({
stroke: '#e4154b',
strokeWidth: that.border_width,
strokeUniform: true,
centeredRotation: true,
});
// disable controls, we currently don't want resizing and do rotation with a button
image.hasControls = false;
// we will resize the image when the initial pdf page is loaded
that.fabricCanvas.add(image);
});
this.fabricCanvas.on({
'object:moving': function (e) {
e.target.opacity = 0.5;
},
'object:modified': function (e) {
e.target.opacity = 1;
},
});
// this.fabricCanvas.on("object:moved", function(opt){ console.log(opt); });
// disallow moving of signature outside of canvas boundaries
this.fabricCanvas.on('object:moving', function (e) {
let obj = e.target;
obj.setCoords();
that.enforceCanvasBoundaries(obj);
});
// TODO: prevent scaling the signature in a way that it is crossing the canvas boundaries
// it's very hard to calculate the way back from obj.scaleX and obj.translateX
// obj.getBoundingRect() will not be updated in the object:scaling event, it is only updated after the scaling is done
// this.fabricCanvas.observe('object:scaling', function (e) {
// let obj = e.target;
//
// console.log(obj);
// console.log(obj.scaleX);
// console.log(obj.translateX);
// });
});
}
enforceCanvasBoundaries(obj) {
// top-left corner
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
}
// bottom-right corner
if (
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
) {
obj.top = Math.min(
obj.top,
obj.canvas.height -
obj.getBoundingRect().height +
obj.top -
obj.getBoundingRect().top
);
obj.left = Math.min(
obj.left,
obj.canvas.width -
obj.getBoundingRect().width +
obj.left -
obj.getBoundingRect().left
);
}
}
async onPageNumberChanged(e) {
let obj = e.target;
const page_no = parseInt(obj.value);
console.log('page_no = ', page_no);
if (page_no > 0 && page_no <= this.totalPages) {
await this.showPage(page_no);
}
}
/**
* Initialize and load the PDF
*
* @param file
* @param isShowPlacement
* @param placementData
*/
async showPDF(file, isShowPlacement = false, placementData = {}) {
let item = this.getSignatureRect();
this.isPageLoaded = false; // prevent redisplay of previous pdf
this.showErrorMessage = false;
// move signature if placementData was set
if (item !== undefined) {
if (placementData['scaleX'] !== undefined) {
item.set('scaleX', placementData['scaleX'] * this.canvasToPdfScale);
}
if (placementData['scaleY'] !== undefined) {
item.set('scaleY', placementData['scaleY'] * this.canvasToPdfScale);
}
if (placementData['left'] !== undefined) {
item.set('left', placementData['left'] * this.canvasToPdfScale);
}
if (placementData['top'] !== undefined) {
item.set('top', placementData['top'] * this.canvasToPdfScale);
}
if (placementData['angle'] !== undefined) {
item.set('angle', placementData['angle']);
}
}
this.isShowPlacement = isShowPlacement;
this.isShowPage = true;
const data = await readBinaryFileContent(file);
// get handle of pdf document
try {
this.pdfDoc = await pdfjs.getDocument({data: data}).promise;
} catch (error) {
console.error(error);
this.showErrorMessage = true;
return;
}
// total pages in pdf
this.totalPages = this.pdfDoc.numPages;
const page = placementData.currentPage || 1;
// show the first page
// if the placementData has no values we want to initialize the signature position
await this.showPage(page, placementData['scaleX'] === undefined);
this.isPageLoaded = true;
// fix width adaption after "this.isPageLoaded = true"
await this.showPage(page);
}
getSignatureRect() {
return this.fabricCanvas.item(0);
}
/**
* Load and render specific page of the PDF
*
* @param pageNumber
* @param initSignature
*/
async showPage(pageNumber, initSignature = false) {
// we need to wait until the last rendering is finished
if (this.isPageRenderingInProgress || this.pdfDoc === null) {
return;
}
const that = this;
this.isPageRenderingInProgress = true;
this.currentPage = pageNumber;
try {
// get handle of page
await this.pdfDoc.getPage(pageNumber).then(async (page) => {
// original width of the pdf page at scale 1
const originalViewport = page.getViewport({scale: 1});
this.currentPageOriginalHeight = originalViewport.height;
// set the canvas width to the width of the container (minus the borders)
this.fabricCanvas.setWidth(this._('#pdf-main-container').clientWidth - 2);
this.canvas.width = this._('#pdf-main-container').clientWidth - 2;
// as the canvas is of a fixed width we need to adjust the scale of the viewport where page is rendered
const oldScale = this.canvasToPdfScale;
this.canvasToPdfScale = this.canvas.width / originalViewport.width;
console.log('this.canvasToPdfScale: ' + this.canvasToPdfScale);
// get viewport to render the page at required scale
const viewport = page.getViewport({scale: this.canvasToPdfScale});
// set canvas height same as viewport height
this.fabricCanvas.setHeight(viewport.height);
this.canvas.height = viewport.height;
// setting page loader height for smooth experience
this._('#page-loader').style.height = this.canvas.height + 'px';
this._('#page-loader').style.lineHeight = this.canvas.height + 'px';
// page is rendered on <canvas> element
const render_context = {
canvasContext: this.canvas.getContext('2d'),
viewport: viewport,
};
let signature = this.getSignatureRect();
// set the initial position of the signature
if (initSignature) {
const sigSizeMM = {width: this.signature_width, height: this.signature_height};
const sigPosMM = {top: 5, left: 5};
const inchPerMM = 0.03937007874;
const DPI = 72;
const pointsPerMM = inchPerMM * DPI;
const documentSizeMM = {
width: originalViewport.width / pointsPerMM,
height: originalViewport.height / pointsPerMM,
};
const sigSize = signature.getOriginalSize();
const scaleX =
(this.canvas.width / sigSize.width) *
(sigSizeMM.width / documentSizeMM.width);
const scaleY =
(this.canvas.height / sigSize.height) *
(sigSizeMM.height / documentSizeMM.height);
const offsetTop = sigPosMM.top * pointsPerMM;
const offsetLeft = sigPosMM.left * pointsPerMM;
signature.set({
scaleX: scaleX,
scaleY: scaleY,
angle: 0,
top: offsetTop,
left: offsetLeft,
lockUniScaling: true, // lock aspect ratio when resizing
});
} else {
// adapt signature scale to new scale
const scaleAdapt = this.canvasToPdfScale / oldScale;
signature.set({
scaleX: signature.get('scaleX') * scaleAdapt,
scaleY: signature.get('scaleY') * scaleAdapt,
left: signature.get('left') * scaleAdapt,
top: signature.get('top') * scaleAdapt,
});
signature.setCoords();
}
// render the page contents in the canvas
try {
await page
.render(render_context)
.promise.then(() => {
console.log('Page rendered');
that.isPageRenderingInProgress = false;
return page.getAnnotations();
})
.then(function (annotationData) {
// remove all child nodes
that.annotationLayer.innerHTML = '';
// update size
that.annotationLayer.style.height = that.canvas.height + 'px';
that.annotationLayer.style.width = that.canvas.width + 'px';
// create all supported annotations
annotationData.forEach((annotation) => {
const subtype = annotation.subtype;
let text = '';
switch (subtype) {
case 'Text':
case 'FreeText':
// Annotations by Adobe Acrobat already have an appearance that can be viewed by pdf.js
if (annotation.hasAppearance) {
return;
}
text = annotation.contents;
break;
case 'Widget':
// Annotations by Adobe Acrobat already have an appearance that can be viewed by pdf.js
if (annotation.hasAppearance) {
return;
}
text = annotation.alternativeText;
break;
default:
// we don't support other types
return;
}
const annotationDiv = document.createElement('div');
const annotationDivInner = document.createElement('div');
annotationDiv.className = 'annotation annotation-' + subtype;
annotationDiv.style.left =
annotation.rect[0] * that.canvasToPdfScale + 'px';
annotationDiv.style.bottom =
annotation.rect[1] * that.canvasToPdfScale + 'px';
annotationDiv.style.width =
(annotation.rect[2] - annotation.rect[0]) *
that.canvasToPdfScale +
'px';
annotationDiv.style.height =
(annotation.rect[3] - annotation.rect[1]) *
that.canvasToPdfScale +
'px';
annotationDivInner.innerText = text === '' ? subtype : text;
annotationDiv.appendChild(annotationDivInner);
that.annotationLayer.appendChild(annotationDiv);
});
// console.log("annotationData render", annotationData);
});
} catch (error) {
console.error(error.message);
that.isPageRenderingInProgress = false;
}
});
} catch (error) {
console.error(error.message);
that.isPageRenderingInProgress = false;
}
}
sendAcceptEvent() {
const item = this.getSignatureRect();
let left = item.get('left');
let top = item.get('top');
const angle = item.get('angle');
// fabricjs includes the stroke in the image position
// and we have to remove it
const border_offset = this.border_width / 2;
if (angle === 0) {
left += border_offset;
top += border_offset;
} else if (angle === 90) {
left -= border_offset;
top += border_offset;
} else if (angle === 180) {
left -= border_offset;
top -= border_offset;
} else if (angle === 270) {
left += border_offset;
top -= border_offset;
}
const data = {
currentPage: this.currentPage,
scaleX: item.get('scaleX') / this.canvasToPdfScale,
scaleY: item.get('scaleY') / this.canvasToPdfScale,
width: (item.get('width') * item.get('scaleX')) / this.canvasToPdfScale,
height: (item.get('height') * item.get('scaleY')) / this.canvasToPdfScale,
left: left / this.canvasToPdfScale,
top: top / this.canvasToPdfScale,
bottom: this.currentPageOriginalHeight - top / this.canvasToPdfScale,
angle: item.get('angle'),
};
const event = new CustomEvent('dbp-pdf-preview-accept', {
detail: data,
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
sendCancelEvent() {
const event = new CustomEvent('dbp-pdf-preview-cancel', {
detail: {},
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}
/**
* Rotates the signature clock-wise in 90� steps
*/
async rotateSignature() {
let signature = this.getSignatureRect();
let angle = (signature.get('angle') + 90) % 360;
signature.rotate(angle);
signature.setCoords();
this.enforceCanvasBoundaries(signature);
// update page to show rotated signature
await this.showPage(this.currentPage);
}
static get styles() {
// language=css
return css`
${commonStyles.getGeneralCSS()}
${commonStyles.getButtonCSS()}
#pdf-meta input[type=number] {
max-width: 50px;
}
#page-loader {
display: flex;
align-items: center;
justify-content: center;
}
/* it's too risky to adapt the height */
/*
#pdf-meta button, #pdf-meta input {
max-height: 15px;
}
*/
#canvas-wrapper {
position: relative;
}
#canvas-wrapper canvas {
position: absolute;
top: 0;
left: 0;
border: var(--dbp-border);
}
#annotation-layer {
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
#annotation-layer > div {
position: absolute;
border: dashed 2px red;
padding: 6px;
background-color: rgba(255, 255, 255, 0.5);
}
#annotation-layer > div > div {
overflow: hidden;
font-size: 0.8em;
height: 100%;
}
.buttons {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: center;
align-items: center;
}
.nav-buttons {
display: flex;
justify-content: center;
flex-grow: 1;
flex-wrap: wrap;
}
.buttons .page-info {
align-self: center;
white-space: nowrap;
}
.nav-buttons > * {
margin: 2px;
}
input[type='number'] {
border: var(--dbp-border);
padding: 0 0.3em;
}
#pdf-meta {
border: var(--dbp-border);
padding: 0.54em;
border-bottom-width: 0;
border-top-width: 0;
}
.button.is-cancel {
color: var(--dbp-danger);
}
.error-message {
text-align: center;
border: 1px solid black;
border-top: 0px;
padding: 15px;
}
#canvas-wrapper canvas#fabric-canvas,
#canvas-wrapper canvas.upper-canvas {
border: unset;
}
input[type='number'] {
background-color: var(--dbp-background);
}
dbp-mini-spinner {
margin: auto;
display: block;
width: 17px;
}
`;
}
render() {
const isRotationHidden = !this.allowSignatureRotation;
const i18n = this._i18n;
return html`
<!--
<form>
<input type="file" name="pdf" id="upload-pdf-input">
</form>
-->
<div id="pdf-main-container" class="${classMap({hidden: !this.isShowPage})}">
<dbp-mini-spinner
class="${classMap({
hidden: this.isPageLoaded || this.showErrorMessage,
})}"></dbp-mini-spinner>
<div
class="error-message ${classMap({
hidden: !this.showErrorMessage || this.isPageLoaded,
})}">
${i18n.t('pdf-preview.error-message')}
</div>
<div class="${classMap({hidden: !this.isPageLoaded})}">
<div id="pdf-meta">
<div class="buttons ${classMap({hidden: !this.isPageLoaded})}">
<button
class="button ${classMap({
hidden: !this.isShowPlacement || isRotationHidden,
})}"
title="${i18n.t('pdf-preview.rotate-signature')}"
@click="${() => {
this.rotateSignature();
}}"
?disabled="${this.isPageRenderingInProgress}">
⟳ ${i18n.t('pdf-preview.rotate')}
</button>
<div class="nav-buttons">
<button
class="button"
title="${i18n.t('pdf-preview.first-page')}"
@click="${async () => {
await this.showPage(1);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === 1}">
<dbp-icon name="angle-double-left"></dbp-icon>
</button>
<button
class="button"
title="${i18n.t('pdf-preview.previous-page')}"
@click="${async () => {
if (this.currentPage > 1)
await this.showPage(--this.currentPage);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === 1}">
<dbp-icon name="chevron-left"></dbp-icon>
</button>
<input
type="number"
min="1"
max="${this.totalPages}"
@input="${this.onPageNumberChanged}"
.value="${live(this.currentPage)}" />
<div class="page-info">
${i18n.t('pdf-preview.page-count', {
totalPages: this.totalPages,
})}
</div>
<button
class="button"
title="${i18n.t('pdf-preview.next-page')}"
@click="${async () => {
if (this.currentPage < this.totalPages)
await this.showPage(++this.currentPage);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === this.totalPages}">
<dbp-icon name="chevron-right"></dbp-icon>
</button>
<button
class="button"
title="${i18n.t('pdf-preview.last-page')}"
@click="${async () => {
await this.showPage(this.totalPages);
}}"
?disabled="${this.isPageRenderingInProgress ||
this.currentPage === this.totalPages}">
<dbp-icon name="angle-double-right"></dbp-icon>
</button>
</div>
<button
class="button is-primary ${classMap({
hidden: !this.isShowPlacement,
})}"
@click="${() => {
this.sendAcceptEvent();
}}">
${i18n.t('pdf-preview.continue')}
</button>
</div>
</div>
<div
id="canvas-wrapper"
class="${classMap({hidden: this.isPageRenderingInProgress})}">
<canvas id="pdf-canvas"></canvas>
<div id="annotation-layer"></div>
<canvas
id="fabric-canvas"
class="${classMap({hidden: !this.isShowPlacement})}"></canvas>
</div>
<div class="${classMap({hidden: !this.isPageRenderingInProgress})}">
<dbp-mini-spinner id="page-loader"></dbp-mini-spinner>
</div>
</div>
</div>
`;
}
}