1

I was hoping to encapsulate some of the Document/Page loading logic in Vue components, but I'm having trouble getting it to work. Here's what I've tried. (In case it's relevant, I'm using Vite + Vue + TS with "pdfjs-dist": "^2.14.305", "vue": "^3.2.25".)

First, I have a component called PDFDocument. It is passed the PDF URL and scale as props, and it handles getting the document and loading the pages.

// PDFDocument.vue
<template>
<div class="pdf-document">
    <PDFPage v-for="page in pages" v-bind="{page, scale}" :key="page.pageNumber"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as pdfjsLib from 'pdfjs-dist';
import PDFPage from './PDFPage.vue';
pdfjsLib.GlobalWorkerOptions.workerSrc = "../../node_modules/pdfjs-dist/build/pdf.worker.js";

export default defineComponent({
    props: {
        url: { type: String, required: true },
        scale: { type: Number, required: true },
    },
    data() {
        let pdf: pdfjsLib.PDFDocumentProxy | undefined;
        let pages: pdfjsLib.PDFPageProxy[] = [];
        return { pdf, pages };
    },
    beforeMount() {
        let loadingTask = pdfjsLib.getDocument(this.url);
        loadingTask.promise.then((pdf) => {
            this.pdf = pdf;
            const promises = [];
            for (let i = 1; i <= pdf.numPages; i++) {
                promises.push(pdf.getPage(i));
            }
            Promise.all(promises).then(pages => {
                this.pages = pages;
                console.log(this.pages);
            });
        });
    },
    components: { PDFPage }
})
</script>

This seems to work, but already I'm encountering my first problem. If I try to use this.pdf.getPage(i) instead of pdf.getPage(i), I get an error: Uncaught (in promise) TypeError: Cannot read from private field. But just using pdf fixes that for now, so okay.

Then I have a PDFPage component that handles setting up the canvas for a page and getting the PDFPageProxy to render onto it.

// PDFPage.vue
<template>
<canvas
    class="pdf-page"
    :width="width"
    :height="height"
    :style="canvasStyle"
></canvas>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = "../../node_modules/pdfjs-dist/build/pdf.worker.js";

export default defineComponent({
    props: {
        page: {type: pdfjsLib.PDFPageProxy, required: true},
        scale: {type: Number, required: true},
    },
    data() {
        let viewport: pdfjsLib.PageViewport | undefined;
        let renderTask: pdfjsLib.RenderTask | undefined;
        return { viewport, renderTask }
    },
    computed: {
        width() { if (!this.viewport) return 0; return this.viewport.width; },
        height() { if (!this.viewport) return 0; return this.viewport.height; },
        canvasStyle() {
            const {width: actualWidth, height: actualHeight} = this.actualSizeViewport;
            const pxRatio = window.devicePixelRatio || 1;
            const [pxWidth, pxHeight] = [actualWidth, actualHeight]
                .map(dim => Math.ceil(dim / pxRatio));
            return `width: ${pxWidth}px; height: ${pxHeight}px;`
        },
        actualSizeViewport() {
            return (this.viewport as pdfjsLib.PageViewport).clone({scale: this.scale});
        },
    },
    methods: {
        drawPage() {
            if (this.renderTask) return;
            const {viewport} = this;
            const canvasContext = this.$el.getContext('2d');
            const renderContext = {canvasContext, viewport};
            this.renderTask = this.page.render(renderContext);
            (this.renderTask as pdfjsLib.RenderTask).promise.then(
                () => this.$emit('rendered', this.page)
            );
        }
    },
    created() {
        this.viewport = this.page.getViewport(this.scale);
    },
    mounted() {
        this.drawPage();
    }
})
</script>

Here, I get the error Uncaught (in promise) TypeError: Cannot read from private field (or sometimes Cannot write to private field) when trying to do this.renderTask = this.page.render(renderContext), which I think is a problem because I'm using this.page.

Does anyone know how to get around this? I feel like it might have to do with the fact that these Proxy objects claim to be proxies to a worker thread, and I might be disrespecting that?

1 Answers1

6

After some digging, I think I've found the culprit. I'm not sure if this is a PDF.js thing or a Vue 3 thing, but the this.pdf and this.page objects are Proxys, not the objects themselves. Importing toRaw from Vue and using toRaw(this.pdf).getPage(i) or toRaw(this.page).render(renderContext) seems to have solved the problem. (Reference: https://stackoverflow.com/a/70805174/18837571.)

(P.S. Also this.page.getViewport in the PDFPage component needs to be this.page.getViewport({scale = this.scale}) for the rendering to work!)