import * as PDFJS from 'pdfjs-dist' import PDFWorker from 'pdfjs-dist/build/pdf.worker.min?url' import { isRef, shallowRef, watch } from 'vue' import type { PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist' import type { Ref } from 'vue' import type { OnPasswordCallback, PDFDestination, PDFInfo, PDFOptions, PDFSrc } from './types' import { getDestinationArray, getDestinationRef, getLocation, isSpecLike } from './utils/destination' import { addStylesToIframe, createIframe } from './utils/miscellaneous' // Could not find a way to make this work with vite, importing the worker entry bundle the whole worker to the the final output // https://erindoyle.dev/using-pdfjs-with-vite/ // PDFJS.GlobalWorkerOptions.workerSrc = PDFWorker function configWorker(wokerSrc: string) { PDFJS.GlobalWorkerOptions.workerSrc = wokerSrc } /** * @typedef {Object} UsePDFParameters * @property {string} password * Document password to unlock content * @property {function} onProgress * Callback to request a password if a wrong or no password was provided. The callback receives two parameters: a function that should be called with the new password, and a reason (see PasswordResponses). * @property {function} onPassword * Callback to be able to monitor the loading progress of the PDF file (necessary to implement e.g. a loading bar). The callback receives an OnProgressParameters argument. if this function is used option.password is ignored * @property {function} onError * Callback to be able to handle errors during loading * */ /** * * @param {string | URL | TypedArray | PDFDataRangeTransport | DocumentInitParameters} src * Can be a URL where a PDF file is located, a typed array (Uint8Array) already populated with data, or a parameter object. * @param {UsePDFParameters} options * UsePDF object parameters */ export function usePDF(src: PDFSrc | Ref, options: PDFOptions = { onProgress: undefined, onPassword: undefined, onError: undefined, password: '', }, ) { if (!PDFJS.GlobalWorkerOptions?.workerSrc) configWorker(PDFWorker) const pdf = shallowRef() const pdfDoc = shallowRef() const pages = shallowRef(0) const info = shallowRef({}) function processLoadingTask(source: PDFSrc) { if (pdfDoc.value) void pdfDoc.value.destroy() const loadingTask = PDFJS.getDocument(source!) if (options.onProgress) loadingTask.onProgress = options.onProgress if (options.onPassword) { loadingTask.onPassword = options.onPassword } else if (options.password) { const onPassword: OnPasswordCallback = (updatePassword, _) => { updatePassword(options.password ?? '') } loadingTask.onPassword = onPassword } loadingTask.promise.then( async (doc) => { pdfDoc.value = doc pdf.value = doc.loadingTask pages.value = doc.numPages const metadata = await doc.getMetadata() const attachments = (await doc.getAttachments()) as Record const javascript = await doc.getJSActions() const outline = await doc.getOutline() info.value = { metadata, attachments, javascript, outline, } }, (error) => { // PDF loading error if (typeof options.onError === 'function') options.onError(error) }, ) } async function getPDFDestination(destination: string | any[] | null): Promise { const document = await pdf.value?.promise if (!document) return null const destArray = await getDestinationArray(document, destination) const destRef = await getDestinationRef(document, destArray) if (!destRef || !destArray) return null const pageIndex = await document.getPageIndex(destRef) const name = destArray[1].name const rest = destArray.slice(2) const location = isSpecLike(rest) ? getLocation(name, rest) : null return { pageIndex, location: location ?? { type: 'Fit', spec: [] } } } async function download(filename = 'filename') { if (!pdfDoc.value) throw new Error('Current PDFDocumentProxy have not loaded yet') const bytes = await pdfDoc.value?.saveDocument() const blobBytes = new Blob([bytes], { type: 'application/pdf' }) const blobUrl = URL.createObjectURL(blobBytes) const anchorDownload = document.createElement('a') document.body.appendChild(anchorDownload) anchorDownload.href = blobUrl anchorDownload.download = filename anchorDownload.style.display = 'none' anchorDownload.click() setTimeout(() => { URL.revokeObjectURL(blobUrl) document.body.removeChild(anchorDownload) }, 10) } async function print(dpi = 150, filename = 'filename') { if (!pdfDoc.value) throw new Error('Current PDFDocumentProxy have not loaded yet') const bytes = await pdfDoc.value?.saveDocument() const savedLoadingTask = PDFJS.getDocument(bytes.buffer) const savedDocument = await savedLoadingTask.promise const PRINT_UNITS = dpi / 72 const CSS_UNITS = 96 / 72 const iframe = await createIframe() const contentWindow = iframe.contentWindow contentWindow!.document.title = filename const pagesNumbers = [...Array(savedDocument.numPages).keys()].map(val => val + 1) for (const pageNumber of pagesNumbers) { const pageToPrint = await savedDocument.getPage(pageNumber) const viewport = pageToPrint.getViewport({ scale: 1 })! if (pageNumber === 1) { addStylesToIframe( contentWindow!, (viewport.width * PRINT_UNITS) / CSS_UNITS, (viewport.height * PRINT_UNITS) / CSS_UNITS, ) } const canvas = document.createElement('canvas') canvas.width = viewport.width * PRINT_UNITS canvas.height = viewport.height * PRINT_UNITS const canvasCloned = canvas.cloneNode() as HTMLCanvasElement contentWindow?.document.body.appendChild(canvasCloned) await pageToPrint?.render({ canvasContext: canvas.getContext('2d')!, intent: 'print', transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], viewport, }).promise canvasCloned.getContext('2d')?.drawImage(canvas, 0, 0) } contentWindow?.focus() contentWindow?.print() document.body.removeChild(iframe) } if (isRef(src)) { if (src.value) processLoadingTask(src.value) watch(src, () => { if (src.value) processLoadingTask(src.value) }) } else { if (src) processLoadingTask(src) } return { pdf, pages, info, print, download, getPDFDestination, } }