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

rollup.config.js

Blame
    • Reiter, Christoph's avatar
      8ac6d855
      Switch to the es5 build of pdf.js · 8ac6d855
      Reiter, Christoph authored
      The current version no longer works with safari 12 and shows an error suggesting
      to use the es5 version instead. So give it a try.
      
      The error: "The browser/environment lacks native support for critical functionality used by the PDF.js library"
      
      The patch for enabling pdf-js to show signatures didn't apply anymore so make the replace pattern
      more generic to match both the es6 and es5 build.
      8ac6d855
      History
      Switch to the es5 build of pdf.js
      Reiter, Christoph authored
      The current version no longer works with safari 12 and shows an error suggesting
      to use the es5 version instead. So give it a try.
      
      The error: "The browser/environment lacks native support for critical functionality used by the PDF.js library"
      
      The patch for enabling pdf-js to show signatures didn't apply anymore so make the replace pattern
      more generic to match both the es6 and es5 build.
    dbp-pdf-preview.js 28.59 KiB
    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-dark);
                }
    
                #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-dark);
                    padding: 0 0.3em;
                }
    
                #pdf-meta {
                    border: var(--dbp-border-dark);
                    padding: 0.54em;
                    border-bottom-width: 0;
                    border-top-width: 0;
                }
    
                .button.is-cancel {
                    color: var(--dbp-danger-dark);
                }
    
                .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;
                }
            `;
        }
    
        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}">
                                    &#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>
            `;
        }
    }