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
« 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.
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.
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"""
14from __future__ import annotations
16import re
17from typing import List, Sequence, Tuple
19_FOLD_RE = re.compile(r"[\s—–:\-]+")
20_QUOTES = str.maketrans({"’": "'", "‘": "'", "“": '"', "”": '"', "„": '"'})
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]
26#: Words whose top edges fall within one band this tall are one printed line.
27LINE_BAND_PTS = 4.0
30def printed_key(text: str) -> str:
31 """The comparison form of printed text."""
32 return _FOLD_RE.sub("", text.translate(_QUOTES)).lower()
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)
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