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 });