Skip to content
Snippets Groups Projects
Select Git revision
  • db314538a128671a83dc4d808f01ba88a2488f71
  • main default protected
  • demo protected
  • master
  • icon-set-mapping
  • production protected
  • revert-62666d1a
  • favorites-and-recent-files
  • lit2
  • wc-part
  • mark-downloaded-files
  • feature/annotpdf-test
  • fix-zip-upload
  • config-cleanup
  • wip
  • app-shell-update
16 results

dbp-pdf-preview.js

Blame
  • Christoph Reiter's avatar
    Reiter, Christoph authored
    It registers an event listener on the window object which keeps a reference
    on the canvas object.
    932c0045
    History
    dbp-pdf-preview.js 25.37 KiB
    import {createI18nInstance} from './i18n.js';
    import {css, html} from 'lit-element';
    import {classMap} from 'lit-html/directives/class-map.js';
    import {live} from 'lit-html/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/es5/build/pdf.js';
    import {name as pkgName} from './../package.json';
    import {readBinaryFileContent} from './utils.js';
    
    const i18n = createI18nInstance();
    
    /**
     * PdfPreview web component
     */
    export class PdfPreview extends ScopedElementsMixin(DBPLitElement) {
        constructor() {
            super();
            this.lang = 'de';
            this.pdfDoc = null;
            this.currentPage = 0;
            this.totalPages = 0;
            this.isShowPage = false;
            this.isPageLoaded = 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 },
                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":
                        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
    
            // 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);
                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: solid 1px black;
                    border-top-color: #888;
                }
    
                #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: 1px solid #000;
                    padding: 0 0.3em;
                }
    
                #pdf-meta {
                    border-color: #000;
                    border-width: 1px;
                    border-style: solid;
                    padding: 0.54em;
                    border-bottom-width: 0;
                    border-top-width: 0;
                }
    
                .button.is-cancel {
                    color: #e4154b;
                }
            `;
        }
    
        render() {
            const isRotationHidden = !this.allowSignatureRotation;
    
            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})}"></dbp-mini-spinner>
                    <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}">&#10227; ${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>
            `;
        }
    }