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/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 }