from __future__ import annotations
import re
import sys
from collections import Counter
from pathlib import Path
import fitz
import spectre_rs
def norm(s: str) -> str:
s = re.sub(r"[*_`]", "", s or "")
s = re.sub(r"\s+", " ", s).strip().lower()
return re.sub(r"[\.\:\,\;\!\?\—\-]+$", "", s)
def main() -> None:
pdf_path = "/mnt/c/Users/RyanJ/spectre-rs/bench-corpus/largedoc/i1040gi.pdf"
pdoc = fitz.open(pdf_path)
sd = spectre_rs.Document(open(pdf_path, "rb").read())
toc = [(t[0], t[1], t[2]) for t in pdoc.get_toc()]
toc_targets: dict[str, tuple[int, int]] = {}
for lvl, title, page in toc:
if title.strip():
toc_targets[norm(title)] = (lvl, page)
pmu_lines_by_page: dict[int, list[tuple[str, float, str]]] = {}
for page_no in range(pdoc.page_count):
page = pdoc[page_no]
d = page.get_text("dict")
lines = []
for blk in d["blocks"]:
if blk.get("type") != 0:
continue
for line in blk.get("lines", []):
line_text = "".join(s["text"] for s in line.get("spans", []))
if not line_text.strip():
continue
max_size = max(
(s["size"] for s in line["spans"]), default=0
)
flags = (
line["spans"][0]["flags"] if line["spans"] else 0
)
font = line["spans"][0]["font"] if line["spans"] else ""
lines.append((line_text, max_size, font))
pmu_lines_by_page[page_no + 1] = lines
spec_blocks_by_page: dict[int, list] = {}
for block in sd.blocks():
spec_blocks_by_page.setdefault(block.page, []).append(block)
categories: Counter[str] = Counter()
examples: dict[str, list[tuple[str, str, str]]] = {
k: [] for k in [
"SAME", "COL_CUT", "LINE_SPLIT", "MERGED_WITH_BODY",
"MISSING", "FONT_SIZE_DIFF",
]
}
for title_norm, (lvl, page) in toc_targets.items():
if len(title_norm) < 4:
continue
candidate_pages = [page] if page in pmu_lines_by_page else []
for p in [page - 1, page, page + 1, page + 2]:
if p in pmu_lines_by_page and p not in candidate_pages:
candidate_pages.append(p)
pmu_match = None
for p in candidate_pages:
for line_text, size, font in pmu_lines_by_page.get(p, []):
if title_norm in norm(line_text):
pmu_match = (p, line_text, size, font)
break
if pmu_match:
break
spec_match = None
spec_partial_blocks: list = []
for p in candidate_pages:
for block in spec_blocks_by_page.get(p, []):
if title_norm in norm(block.text):
for line in block.lines:
if title_norm in norm(line.text):
spec_match = (
p, line.text,
max((s.font_size for s in line.spans), default=0),
block,
)
break
if spec_match:
break
title_words = title_norm.split()
if title_words:
first_words = " ".join(title_words[:2])
if first_words in norm(block.text):
spec_partial_blocks.append((p, block))
if spec_match:
break
if not pmu_match:
continue if not spec_match:
if spec_partial_blocks:
categories["COL_CUT"] += 1
if len(examples["COL_CUT"]) < 5:
examples["COL_CUT"].append((
title_norm,
pmu_match[1][:60],
f"partials: {len(spec_partial_blocks)} blocks",
))
else:
categories["MISSING"] += 1
if len(examples["MISSING"]) < 5:
examples["MISSING"].append((
title_norm,
pmu_match[1][:60],
"no match",
))
continue
pmu_p, pmu_text, pmu_size, pmu_font = pmu_match
spec_p, spec_text, spec_size, spec_block = spec_match
if abs(pmu_size - spec_size) > 1.5:
categories["FONT_SIZE_DIFF"] += 1
if len(examples["FONT_SIZE_DIFF"]) < 5:
examples["FONT_SIZE_DIFF"].append((
title_norm,
f"pmu {pmu_size:.0f}pt",
f"spec {spec_size:.0f}pt",
))
continue
spec_dom_block_size = 0.0
if spec_block.lines:
char_by_size: Counter[float] = Counter()
for ln in spec_block.lines:
for sp in ln.spans:
char_by_size[round(sp.font_size)] += len(sp.text)
if char_by_size:
spec_dom_block_size = char_by_size.most_common(1)[0][0]
if spec_dom_block_size and abs(spec_dom_block_size - spec_size) > 1.5:
categories["MERGED_WITH_BODY"] += 1
if len(examples["MERGED_WITH_BODY"]) < 5:
examples["MERGED_WITH_BODY"].append((
title_norm,
f"spec line {spec_size:.0f}pt",
f"block dom {spec_dom_block_size:.0f}pt",
))
continue
if len(spec_block.lines) > 1:
categories["LINE_SPLIT"] += 1
if len(examples["LINE_SPLIT"]) < 5:
examples["LINE_SPLIT"].append((
title_norm,
pmu_text[:60],
f"spec block has {len(spec_block.lines)} lines",
))
continue
categories["SAME"] += 1
if len(examples["SAME"]) < 5:
examples["SAME"].append((
title_norm, pmu_text[:60], f"{spec_size:.0f}pt",
))
print("=" * 80)
print("Segmentation diff: spectre-rs vs pymupdf on i1040gi.pdf")
print("=" * 80)
print(f"Total TOC entries analyzed: {sum(categories.values())}")
print()
for cat in [
"SAME", "MERGED_WITH_BODY", "COL_CUT", "LINE_SPLIT",
"FONT_SIZE_DIFF", "MISSING",
]:
count = categories[cat]
pct = count / max(sum(categories.values()), 1) * 100
print(f" {cat:<20} {count:>5} ({pct:>5.1f}%)")
print()
print("Examples per category:")
for cat, ex in examples.items():
if not ex:
continue
print(f"\n {cat}:")
for title, a, b in ex:
print(f" {title!r:<50} | {a} | {b}")
if __name__ == "__main__":
main()