Coverage for src / quber / core / printed_text.py: 100%

27 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-09-23 22:14 -0400

1"""Text that has to be printed on the page to be kept. 

2 

3The vetting agent copies a table's caption and title off the page. A copy is 

4accepted only when the page's text layer contains it, the rule values already 

5follow: anything the page does not print was read off nothing. 

6 

7`printed_key` folds what rendering changes and reading does not. Whitespace, 

8dashes and colons are dropped, so a title printed over two lines or a footnote 

9digit set as a superscript still matches; curly quotes and apostrophes become 

10straight; case is dropped. Two strings with the same key are the same printed 

11words. 

12""" 

13 

14from __future__ import annotations 

15 

16import re 

17from typing import List, Sequence, Tuple 

18 

19_FOLD_RE = re.compile(r"[\s—–:\-]+") 

20_QUOTES = str.maketrans({"’": "'", "‘": "'", "“": '"', "”": '"', "„": '"'}) 

21 

22#: One word of the page text layer: x0, top, x1, bottom in points with a 

23#: top-left origin, then the word. 

24WordBox = Tuple[float, float, float, float, str] 

25 

26#: Words whose top edges fall within one band this tall are one printed line. 

27LINE_BAND_PTS = 4.0 

28 

29 

30def printed_key(text: str) -> str: 

31 """The comparison form of printed text.""" 

32 return _FOLD_RE.sub("", text.translate(_QUOTES)).lower() 

33 

34 

35def is_printed(value: str, page_text: str) -> bool: 

36 """Whether `value` occurs in `page_text`, compared by printed key. Empty 

37 text is not printed.""" 

38 key = printed_key(value) 

39 return bool(key) and key in printed_key(page_text) 

40 

41 

42def page_lines(words: Sequence[WordBox]) -> List[str]: 

43 """The page's words as printed lines in reading order, top to bottom and 

44 left to right. Two tables printed side by side share lines, which is how 

45 the page prints them.""" 

46 ordered = sorted(words, key=lambda w: (round(w[1] / LINE_BAND_PTS), w[0])) 

47 lines: List[str] = [] 

48 current: List[str] = [] 

49 band = None 

50 for w in ordered: 

51 b = round(w[1] / LINE_BAND_PTS) 

52 if band is not None and b != band: 

53 lines.append(" ".join(current)) 

54 current = [] 

55 current.append(w[4]) 

56 band = b 

57 if current: 

58 lines.append(" ".join(current)) 

59 return lines