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


test/e2e/ocr-ppm.test.ts (3430 bytes)

1 // End-to-end check without a browser: tesseract.js reads the same symbols
2 // from the PNG file of a page and from a PPM file of the same pixels. The app
3 // gives tesseract.js a PPM file (src/ppm.ts), because the PNG encoding of the
4 // page canvas took about 13 s for each page in the Android WebView.
5 // Needs network on the first run to fetch language data into .tessdata/.
6 // Run: npm run test:e2e
7 
8 import { mkdir, readFile } from 'node:fs/promises';
9 import path from 'node:path';
10 import sharp from 'sharp';
11 import { createWorker } from 'tesseract.js';
12 import { describe, expect, it } from 'vitest';
13 import type { OcrToken } from '../../src/align';
14 import { pageToTokens } from '../../src/ocr-tokens';
15 import { imageToPpm } from '../../src/ppm';
16 
17 const TIMEOUT = 300_000;
18 const CACHE = '.tessdata';
19 
20 /** A PPM file of the pixels of a PNG file: RGBA pixels, as getImageData gives them in the app. */
21 async function ppmOf(png: Buffer): Promise<Buffer> {
22   const { data, info } = await sharp(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
23   expect(info.channels).toBe(4);
24   const ppm = imageToPpm({
25     width: info.width,
26     height: info.height,
27     data: new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
28   });
29   return Buffer.from(ppm.buffer, ppm.byteOffset, ppm.byteLength);
30 }
31 
32 /** How far apart two readings of one page are. */
33 function difference(a: OcrToken[], b: OcrToken[]): string {
34   const text = (tokens: OcrToken[]) => tokens.map((t) => t.text).join('');
35   let box = 0;
36   let unequal = 0;
37   for (let i = 0; i < Math.min(a.length, b.length); i++) {
38     const p = a[i]!;
39     const q = b[i]!;
40     const d = Math.max(
41       Math.abs(p.bbox.x0 - q.bbox.x0),
42       Math.abs(p.bbox.y0 - q.bbox.y0),
43       Math.abs(p.bbox.x1 - q.bbox.x1),
44       Math.abs(p.bbox.y1 - q.bbox.y1),
45     );
46     box = Math.max(box, d);
47     if (d > 0 || p.text !== q.text || p.line !== q.line || p.word !== q.word) unequal++;
48   }
49   return (
50     `${a.length} and ${b.length} symbols, text ${text(a) === text(b) ? 'equal' : 'not equal'}, ` +
51     `${unequal} unequal symbols, largest box difference ${box} px`
52   );
53 }
54 
55 describe.each([
56   { file: 'sample-eng-2p-1.png', lang: 'eng' },
57   { file: 'sample-jpn.png', lang: 'jpn' },
58 ])('OCR of $file', ({ file, lang }) => {
59   it(
60     'reads the same symbols from the PNG file and from a PPM file of its pixels',
61     async () => {
62       const png = await readFile(path.resolve('fixtures', file));
63       const ppm = await ppmOf(png);
64       // tesseract.js writes the cache file only when the directory exists.
65       await mkdir(CACHE, { recursive: true });
66       const worker = await createWorker(lang, 1, { cachePath: CACHE });
67       try {
68         const read = async (image: Buffer): Promise<OcrToken[]> => {
69           const start = performance.now();
70           const { data } = await worker.recognize(image, {}, { blocks: true, text: false });
71           console.log(
72             `[${file}] ${image.length} bytes read in ${Math.round(performance.now() - start)} ms`,
73           );
74           return pageToTokens(data, 0);
75         };
76         const fromPng = await read(png);
77         const fromPpm = await read(ppm);
78         console.log(`[${file}] PNG and PPM: ${difference(fromPng, fromPpm)}`);
79         expect(fromPng.length).toBeGreaterThan(0);
80         expect(fromPpm).toEqual(fromPng);
81       } finally {
82         await worker.terminate();
83       }
84     },
85     TIMEOUT,
86   );
87 });