src/line-boxes.ts (3681 bytes)
1 // The boxes that mark a cue on the page. Pure: no DOM. 2 3 import type { OcrToken, TokenSpan } from './align'; 4 import { isCjk } from './book-text'; 5 6 export type Box = OcrToken['bbox']; 7 8 /** A token of punctuation or symbols only. */ 9 const PUNCTUATION = /^[\p{P}\p{S}]+$/u; 10 11 /** A token of opening punctuation only: a bracket or a quote that opens. */ 12 const OPENING = /^[\p{Ps}\p{Pi}]+$/u; 13 14 /** 15 * True when the box of a line can grow from the edge token of the span 16 * over `next`, the token before it (`back`) or after it, on the same line. 17 * 18 * After a letter that is not CJK, the tokens of the same word join: a 19 * space, which is no token, gives the next word another word id. CJK has 20 * no spaces, and its OCR words do not show where a word ends: Japanese OCR 21 * can give one word for a whole line, or one for a 。 alone. So after a 22 * CJK character only punctuation joins: an opening mark before the span, 23 * and another mark after it. A 。 before the span ends the cue before. 24 */ 25 function joins(edge: OcrToken, next: OcrToken | undefined, back: boolean): next is OcrToken { 26 if (next === undefined || next.page !== edge.page || next.line !== edge.line) return false; 27 if (!isCjk(edge.text)) { 28 return ( 29 edge.word !== undefined && 30 next.word === edge.word && 31 (!isCjk(next.text) || PUNCTUATION.test(next.text)) 32 ); 33 } 34 if (back) return OPENING.test(next.text); 35 return PUNCTUATION.test(next.text) && !OPENING.test(next.text); 36 } 37 38 /** Grows `box` so that it holds `bbox`. */ 39 function grow(box: Box, bbox: Box): void { 40 box.x0 = Math.min(box.x0, bbox.x0); 41 box.y0 = Math.min(box.y0, bbox.y0); 42 box.x1 = Math.max(box.x1, bbox.x1); 43 box.y1 = Math.max(box.y1, bbox.y1); 44 } 45 46 /** 47 * The share of the line height that pads the left and the right side of a 48 * box. Tesseract symbol boxes can end some pixels before the ink of a 49 * letter, more in a larger font. A quarter of the line height is about the 50 * width of a word space, so the box does not reach the next word. 51 */ 52 export const SIDE_PAD = 0.25; 53 54 /** 55 * `box` with `pad` pixels on each side. The left and the right side get 56 * SIDE_PAD of the height of the box when that is more. 57 */ 58 export function padBox(box: Box, pad: number): Box { 59 const side = Math.max(pad, SIDE_PAD * (box.y1 - box.y0)); 60 return { x0: box.x0 - side, y0: box.y0 - pad, x1: box.x1 + side, y1: box.y1 + pad }; 61 } 62 63 /** 64 * One box for each text line of `span` on `page`: the union of the boxes 65 * of its tokens on that line. Tokens on other pages do not count. A span of 66 * an older alignment can go past the end of `tokens`: those indices do not 67 * count either. 68 * 69 * The alignment drops punctuation, and it can drop a noisy letter at the 70 * edge of a cue. So the box of a line grows to the edges of its first and 71 * its last word (see `joins`). 72 */ 73 export function lineBoxes(tokens: readonly OcrToken[], span: TokenSpan, page: number): Box[] { 74 /** For each line: its box, and the first and the last token of the span on it. */ 75 const lines = new Map<number, { box: Box; first: number; last: number }>(); 76 for (let t = span.start; t < span.end; t++) { 77 const token = tokens[t]; 78 if (!token || token.page !== page) continue; 79 const line = lines.get(token.line); 80 if (!line) lines.set(token.line, { box: { ...token.bbox }, first: t, last: t }); 81 else { 82 grow(line.box, token.bbox); 83 line.last = t; 84 } 85 } 86 return [...lines.values()].map(({ box, first, last }) => { 87 for (let t = first - 1; joins(tokens[first]!, tokens[t], true); t--) grow(box, tokens[t]!.bbox); 88 for (let t = last + 1; joins(tokens[last]!, tokens[t], false); t++) grow(box, tokens[t]!.bbox); 89 return box; 90 }); 91 }