src/pdf.ts (2587 bytes)
1 // pdf.js wrapper: open a PDF, render one page to a canvas, read its text layer. 2 3 import * as pdfjs from 'pdfjs-dist'; 4 import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; 5 import type { OcrToken } from './align'; 6 import { MIN_TEXT_CHARS, textChars, textItems, textItemsToTokens } from './text-layer'; 7 8 pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; 9 10 export interface PageText { 11 /** Null when the page has no useful text layer. */ 12 tokens: OcrToken[] | null; 13 /** Size of the page in pixels at `targetWidth`. */ 14 width: number; 15 height: number; 16 } 17 18 export interface PdfDoc { 19 numPages: number; 20 /** Render page `index` (0-based) so that its width is `targetWidth` pixels. */ 21 renderPage(index: number, targetWidth: number): Promise<HTMLCanvasElement>; 22 /** The text layer of page `index` as tokens in the pixel space of `targetWidth`. */ 23 pageText(index: number, targetWidth: number): Promise<PageText>; 24 destroy(): Promise<void>; 25 } 26 27 export async function loadPdf(data: ArrayBuffer): Promise<PdfDoc> { 28 const task = pdfjs.getDocument({ data }); 29 const doc = await task.promise; 30 const viewportOf = async (index: number, targetWidth: number) => { 31 const page = await doc.getPage(index + 1); 32 const base = page.getViewport({ scale: 1 }); 33 return { page, viewport: page.getViewport({ scale: targetWidth / base.width }) }; 34 }; 35 return { 36 numPages: doc.numPages, 37 async renderPage(index, targetWidth) { 38 const { page, viewport } = await viewportOf(index, targetWidth); 39 const canvas = document.createElement('canvas'); 40 canvas.width = Math.ceil(viewport.width); 41 canvas.height = Math.ceil(viewport.height); 42 // The "display" intent paces rendering with requestAnimationFrame, 43 // which stops in a hidden tab. OCR runs for a long time, so use the 44 // "print" intent, which renders without it. 45 await page.render({ canvas, viewport, intent: 'print' }).promise; 46 return canvas; 47 }, 48 async pageText(index, targetWidth) { 49 const { page, viewport } = await viewportOf(index, targetWidth); 50 const content = await page.getTextContent(); 51 const items = textItems(content.items); 52 const size = { width: Math.ceil(viewport.width), height: Math.ceil(viewport.height) }; 53 if (textChars(items) < MIN_TEXT_CHARS) return { tokens: null, ...size }; 54 const toPixel = (x: number, y: number) => 55 viewport.convertToViewportPoint(x, y) as [number, number]; 56 return { tokens: textItemsToTokens(items, index, toPixel), ...size }; 57 }, 58 destroy: () => task.destroy(), 59 }; 60 }