Recently Written · git

scan-subread

PDF reader that follows an audiobook with the SubRead suite

git clone https://github.com/equwal/scan-subread

Log | Files | Refs


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 }