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/text-layer.test.ts (4016 bytes)

1 import { describe, expect, it } from 'vitest';
2 import { bookText } from '../src/book-text';
3 import { textChars, textItemsToTokens, type TextItemLike } from '../src/text-layer';
4 
5 /** A page 200 pt wide and 300 pt high, rendered 2 px per pt, y flipped. */
6 const toPixel = (x: number, y: number): [number, number] => [2 * x, 2 * (300 - y)];
7 
8 function item(over: Partial<TextItemLike> & Pick<TextItemLike, 'str'>): TextItemLike {
9   return {
10     dir: 'ltr',
11     transform: [12, 0, 0, 12, 10, 100],
12     width: 60,
13     height: 12,
14     hasEOL: false,
15     ...over,
16   };
17 }
18 
19 type Box = { x0: number; y0: number; x1: number; y1: number };
20 
21 function expectBox(box: Box, expected: Box): void {
22   for (const k of ['x0', 'y0', 'x1', 'y1'] as const) expect(box[k], k).toBeCloseTo(expected[k], 6);
23 }
24 
25 describe('textItemsToTokens', () => {
26   it('splits a horizontal run into one box per character, left to right', () => {
27     const tokens = textItemsToTokens([item({ str: 'abc', width: 30 })], 0, toPixel);
28     expect(tokens.map((t) => t.text)).toEqual(['a', 'b', 'c']);
29     expect(tokens.map((t) => t.line)).toEqual([0, 0, 0]);
30     // 10 pt each, from x = 10; the box spans 0.25 below to 0.8 above the baseline y = 100.
31     expectBox(tokens[0]!.bbox, { x0: 20, y0: 2 * (300 - 109.6), x1: 40, y1: 2 * (300 - 97) });
32     expect(tokens[1]!.bbox.x0).toBeCloseTo(40, 6);
33     expect(tokens[2]!.bbox.x1).toBeCloseTo(80, 6);
34   });
35 
36   it('stacks the characters of a vertical font down the page', () => {
37     // A vertical run at x = 150 from y = 250 down, 14 pt per glyph.
38     const tokens = textItemsToTokens(
39       [
40         item({
41           str: '吾輩は',
42           dir: 'ttb',
43           transform: [14, 0, 0, 14, 150, 250],
44           width: 14,
45           height: 42,
46         }),
47       ],
48       0,
49       toPixel,
50     );
51     expect(tokens.map((t) => t.text)).toEqual(['吾', '輩', 'は']);
52     expectBox(tokens[0]!.bbox, {
53       x0: 2 * 143,
54       y0: 2 * (300 - 250),
55       x1: 2 * 157,
56       y1: 2 * (300 - 236),
57     });
58     expect(tokens[1]!.bbox.y0).toBeCloseTo(2 * (300 - 236), 6);
59     expect(tokens[2]!.bbox.y1).toBeCloseTo(2 * (300 - 208), 6);
60     expect(new Set(tokens.map((t) => t.line)).size).toBe(1);
61   });
62 
63   it('lays a run rotated by 90 degrees along y', () => {
64     // Text that reads upward: the x axis of the font points up the page.
65     const tokens = textItemsToTokens(
66       [item({ str: 'ab', transform: [0, 12, -12, 0, 50, 100], width: 24, height: 12 })],
67       0,
68       toPixel,
69     );
70     expect(tokens[0]!.bbox.y0).toBeGreaterThan(tokens[1]!.bbox.y0);
71     expect(tokens[0]!.bbox.x0).toBeCloseTo(tokens[1]!.bbox.x0, 6);
72   });
73 
74   it('gives two baselines two lines, and one baseline one line', () => {
75     const tokens = textItemsToTokens(
76       [
77         item({ str: 'The ', width: 40 }),
78         item({ str: 'sun', width: 30, transform: [12, 0, 0, 12, 50, 101] }),
79         item({ str: 'rose', width: 40, transform: [12, 0, 0, 12, 10, 80] }),
80       ],
81       1,
82       toPixel,
83     );
84     expect(tokens.map((t) => t.text).join('')).toBe('Thesunrose');
85     const lines = tokens.map((t) => t.line);
86     expect(lines.slice(0, 6).every((l) => l === lines[0])).toBe(true);
87     expect(lines[6]).not.toBe(lines[0]);
88     expect(lines[0]).toBe(100000);
89     expect(bookText(tokens)).toBe('The sun\nrose\n');
90   });
91 
92   it('starts a new line after hasEOL, even on the same baseline', () => {
93     const tokens = textItemsToTokens(
94       [item({ str: 'ab', hasEOL: true }), item({ str: 'cd' })],
95       0,
96       toPixel,
97     );
98     expect(tokens[0]!.line).not.toBe(tokens[2]!.line);
99   });
100 
101   it('drops spaces and bumps the word id at each', () => {
102     const tokens = textItemsToTokens([item({ str: 'a b' })], 0, toPixel);
103     expect(tokens.map((t) => t.text)).toEqual(['a', 'b']);
104     expect(tokens[0]!.word).not.toBe(tokens[1]!.word);
105   });
106 });
107 
108 describe('textChars', () => {
109   it('counts the non-space characters', () => {
110     expect(textChars([item({ str: 'a b ' }), item({ str: ' ' }), item({ str: 'cd' })])).toBe(4);
111   });
112 });