import * as pdfjsLib from 'pdfjs-dist'; import { ScaledSize, RenderRequest, RenderResult } from './types'; import { Mutex, MutexInterface, withTimeout } from 'async-mutex'; export default class PdfDocument { public url: string | Uint8Array; private doc: any = null; public pageCount: number = null; private pages: any = {}; private rendererMutex: MutexInterface = null; private pageSize: { height: number; width: number; } = null; private document: HTMLDocument = null; public constructor(document: HTMLDocument) { this.document = document; this.rendererMutex = withTimeout(new Mutex(), 40 * 1000); } public loadDoc = async (url: string | Uint8Array) => { this.url = url; const loadingTask = pdfjsLib.getDocument(url); try { const pdfDocument: any = await loadingTask.promise; this.doc = pdfDocument; this.pageCount = pdfDocument.numPages; } catch (error) { error.message = `Could not load document: ${error.message}`; throw error; } }; public getPage = async (pageNo: number) => { if (!this.doc) { throw new Error('Document not loaded'); } if (!this.pages[pageNo]) { this.pages[pageNo] = await this.doc.getPage(pageNo); } return this.pages[pageNo]; }; public getPageSize = async () => { if (!this.pageSize) { const page = await this.getPage(1); const viewport = page.getViewport({ scale: 1.0 }); this.pageSize = { height: viewport.height, width: viewport.width, }; } return this.pageSize; }; public getScaledSize = async (height: number = null, width: number = null): Promise => { const actualSize = await this.getPageSize(); let scale = 1.0; if (height && width) { scale = Math.min(height / actualSize.height, width / actualSize.width); } else if (height) { scale = height / actualSize.height; } else if (width) { scale = width / actualSize.width; } return { height: actualSize.height * scale, width: actualSize.width * scale, scale, }; }; public getActivePageNo = (scaledSize: ScaledSize, pageGap: number, scrollTop: number): number => { const pageHeight = scaledSize.height + pageGap; const pageNo = Math.floor(scrollTop / pageHeight) + 1; return Math.min(pageNo, this.pageCount); }; private renderPageImpl = async ({ pageNo, scaledSize, getTextLayer, isCancelled }: RenderRequest): Promise => { const checkCancelled = () => { if (isCancelled()) { throw new Error(`Render cancelled, page: ${pageNo}`); } }; const page = await this.getPage(pageNo); checkCancelled(); const canvas = this.document.createElement('canvas'); canvas.classList.add('page-canvas'); const viewport = page.getViewport({ scale: scaledSize.scale || 1.0 }); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Could not get canvas context'); } await page.render({ canvasContext: ctx, viewport, }).promise; checkCancelled(); let textLayerDiv = null; if (getTextLayer) { textLayerDiv = this.document.createElement('div'); textLayerDiv.classList.add('textLayer'); const textFragment = this.document.createDocumentFragment(); const txtContext = await page.getTextContent(); checkCancelled(); // Pass the data to the method for rendering of text over the pdf canvas. textLayerDiv.style.height = `${viewport.height}px`; textLayerDiv.style.width = `${viewport.width}px`; await pdfjsLib.renderTextLayer({ textContent: txtContext, enhanceTextSelection: true, container: textFragment, viewport: viewport, textDivs: [], }).promise; textLayerDiv.appendChild(textFragment); } return { canvas, textLayerDiv }; }; public async renderPage(task: RenderRequest): Promise { // We're using a render mutex to avoid rendering too many pages at the same time // Which can cause the pdfjs library to abandon some of the in-progress rendering unexpectedly return await this.rendererMutex.runExclusive(async () => { return await this.renderPageImpl(task); }); } public printPdf = () => { const frame = this.document.createElement('iframe'); frame.style.position = 'fixed'; frame.style.display = 'none'; frame.style.height = '100%'; frame.style.width = '100%'; this.document.body.appendChild(frame); frame.onload = () => { frame.contentWindow.onafterprint = () => { frame.remove(); }; frame.focus(); frame.contentWindow.print(); }; frame.src = this.url as string; }; public downloadPdf = async () => { const url = this.url as string; const link = this.document.createElement('a'); link.href = url; link.download = url; link.click(); link.remove(); }; }