Skip to content
Snippets Groups Projects
Select Git revision
  • 08143b0906733f72f61122dd5dd8c3eaafc5c01a
  • 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

vpu-pdf-preview.js

Blame
  • vpu-pdf-preview.js 18.44 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 VPULitElement from 'vpu-common/vpu-lit-element';
    import {MiniSpinner} from 'vpu-common';
    import * as commonUtils from "vpu-common/utils";
    import * as commonStyles from 'vpu-common/styles';
    import pdfjs from 'pdfjs-dist';
    import {fabric} from 'fabric';
    
    const i18n = createI18nInstance();
    
    /**
     * PdfPreview web component
     */
    export class PdfPreview extends ScopedElementsMixin(VPULitElement) {
        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.fabricCanvas = null;
            this.canvasToPdfScale = 1.0;
            this.sigImageOriginalWidth = 0;
            this.currentPageOriginalHeight = 0;
        }
    
        static get scopedElements() {
            return {
                'vpu-mini-spinner': MiniSpinner,
            };
        }
    
        /**
         * See: https://lit-element.polymer-project.org/guide/properties#initialize
         */
        static get properties() {
            return {
                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 },
            };
        }
    
        update(changedProperties) {
            changedProperties.forEach((oldValue, propName) => {
                switch (propName) {
                    case "lang":
                        i18n.changeLanguage(this.lang);
                        break;
                }
            });
    
            super.update(changedProperties);
        }
    
        connectedCallback() {
            super.connectedCallback();
            const that = this;
            pdfjs.GlobalWorkerOptions.workerSrc = commonUtils.getAssetURL('local/vpu-signature/pdfjs/pdf.worker.min.js');
    
            this.updateComplete.then(() => {
                that.canvas = that._('#pdf-canvas');
    
                // this._('#upload-pdf-input').addEventListener('change', function() {
                //     that.showPDF(this.files[0]);
                // });
    
                // redraw page if window was resized
                window.onresize = async () => {
                    await that.showPage(that.currentPage);
                };
    
                // Safari can't use that yet
                // new ResizeObserver(async () => {
                //     await that.showPage(that.currentPage);
                // }).observe(this._('#pdf-main-container'));
    
                // add fabric.js canvas for signature positioning
                // , {stateful : true}
                this.fabricCanvas = new fabric.Canvas(this._('#fabric-canvas'));
    
                // add signature image
                fabric.Image.fromURL(commonUtils.getAssetURL('local/vpu-signature/signature-placeholder.png'), function(image) {
                    that.sigImageOriginalWidth = image.width;
    
                    // add a red border around the signature placeholder
                    image.set({stroke: "#e4154b", strokeWidth: 3});
    
                    // TODO: un-lock rotation when rotation point in PDF-AS is matched
                    image.lockRotation = true;
    
                    // TODO: turn on controls when we enable rotation and resizing again
                    image.hasControls = false;
    
                    // we will resize the image when the initial pdf page is loaded
                    that.fabricCanvas.add(image);
                });
    
                // 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();
    
                    // 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);
                    }
                });
    
                // 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);
                // });
            });
        }
    
        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();
    
            // 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;
            let reader = new FileReader();
    
            reader.onload = async () => {
                this.isPageLoaded = false;
                const data = reader.result;
    
                // 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);
            };
    
            reader.readAsBinaryString(file);
        }
    
        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 });
                    const pdfOriginalWidth = originalViewport.width;
                    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 / pdfOriginalWidth;
    
                    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) {
                        // We calculate the scale from a fixed with that mimics the width that is used by PDF-AS
                        // There currently seems to be no way in pdf.js to get the width in inch or mm so we are assuming
                        // pixel from a default A4 document (width in portrait, height in landscape)
                        const docSize = Math.max(originalViewport.width, originalViewport.height);
                        const scale = 128.1 / docSize;
    
                        signature.set({
                            scaleX: scale,
                            scaleY: scale,
                            angle: 0,
                            top: 0,
                            left: 0,
                            lockUniScaling: true  // lock aspect ratio when resizing
                        });
    
                        // we want to center the signature at the page
                        // signature.center();
                    } 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;
                        });
                    } catch(error) {
                        console.error(error.message);
                        that.isPageRenderingInProgress = false;
                    }
                });
            } catch(error) {
                console.error(error.message);
                that.isPageRenderingInProgress = false;
            }
        }
    
        sendAcceptEvent() {
            const item = this.getSignatureRect();
            const data = {
                "currentPage": this.currentPage,
                "currentPageOriginalHeight": this.currentPageOriginalHeight,
                "scaleX": item.get("scaleX") / this.canvasToPdfScale,
                "scaleY": item.get("scaleY") / this.canvasToPdfScale,
                "width": item.get("width") * item.get("scaleY") / this.canvasToPdfScale,
                "height": item.get("height") * item.get("scaleY") / this.canvasToPdfScale,
                "left": item.get("left") / this.canvasToPdfScale,
                "top": item.get("top") / this.canvasToPdfScale,
                "angle": item.get("angle")
            };
            const event = new CustomEvent("vpu-pdf-preview-accept",
                { "detail": data, bubbles: true, composed: true });
            this.dispatchEvent(event);
        }
    
        sendCancelEvent() {
            const event = new CustomEvent("vpu-pdf-preview-cancel",
                { "detail": {}, bubbles: true, composed: true });
            this.dispatchEvent(event);
        }
    
        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;
                }
    
                .buttons {
                    display: flex;
                    flex-wrap: wrap;
                }
    
                .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;
                }
    
                .page-info {
                    white-space: nowrap;
                    margin: auto 0 auto auto;
                }
    
                .button.is-cancel {
                    color: #e4154b;
                }
            `;
        }
    
        render() {
            return html`
    <!--
                <form>
                    <input type="file" name="pdf" id="upload-pdf-input">
                </form>
    -->
                <div id="pdf-main-container" class="${classMap({hidden: !this.isShowPage})}">
                    <vpu-mini-spinner class="${classMap({hidden: this.isPageLoaded})}"></vpu-mini-spinner>
                    <div class="${classMap({hidden: !this.isPageLoaded})}">
                        <div id="pdf-meta">
                            <div class="buttons">
                                <button class="button"
                                        title="${i18n.t('pdf-preview.first-page')}"
                                        @click="${async () => { await this.showPage(1); } }"
                                        ?disabled="${this.isPageRenderingInProgress || this.currentPage === 1}">${i18n.t('pdf-preview.first')}</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}">${i18n.t('pdf-preview.previous')}</button>
                                <input type="number"
                                       min="1"
                                       max="${this.totalPages}"
                                       @input="${this.onPageNumberChanged}"
                                       .value="${live(this.currentPage)}">
                                <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}">${i18n.t('pdf-preview.next')}</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}">${i18n.t('pdf-preview.last')}</button>
                                <button class="button is-primary ${classMap({hidden: !this.isShowPlacement})}"
                                        @click="${() => { this.sendAcceptEvent(); } }">${i18n.t('pdf-preview.continue')}</button>
    <!--
                                <button class="button is-cancel"
                                        @click="${() => { this.sendCancelEvent(); } }">${i18n.t('pdf-preview.cancel')}</button>
    -->
    
                                <div class="page-info">${i18n.t('pdf-preview.page-count', {currentPage: this.currentPage, totalPages: this.totalPages, })}</div>
                            </div>
                        </div>
                        <div id="canvas-wrapper" class="${classMap({hidden: this.isPageRenderingInProgress})}">
                            <canvas id="pdf-canvas"></canvas>
                            <canvas id="fabric-canvas" class="${classMap({hidden: !this.isShowPlacement})}"></canvas>
                        </div>
                        <div class="${classMap({hidden: !this.isPageRenderingInProgress})}"><vpu-mini-spinner id="page-loader"></vpu-mini-spinner></div>
                    </div>
                </div>
            `;
        }
    }