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