Derives page layout from the file itself (batch size, column edges, row grid, labelling font), places text by start-x to survive cell overflow, detects blackout redactions including filled-path bars and single-character bars, and self-verifies via Excel's printed row-number chain. Output byte-identical to four independently written implementations across a 1,048,576-row reference file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012bQ28wRBiQYcSR9FzV9U7c
66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
"""Reading what the PDF actually drew on a page.
|
|
|
|
Small views onto the raw page. Everything downstream is built from these.
|
|
The key one is all_runs(): it returns each piece of text AS THE PDF DREW IT,
|
|
one run per spreadsheet cell, which is what dodges Trap 1 -- overlapping
|
|
cells stay separate because the PDF drew them separately.
|
|
"""
|
|
|
|
|
|
def all_runs(page):
|
|
"""Every piece of text on the page, one run per spreadsheet cell."""
|
|
return [r for r in page.get_texttrace() if r["chars"]]
|
|
|
|
|
|
def run_start(run):
|
|
"""Where a piece of text begins, as (across, down)."""
|
|
return run["chars"][0][2]
|
|
|
|
|
|
def run_text(run):
|
|
"""The text of a run. Word Wrap would really screw this up; don't let
|
|
the PDF boomers know."""
|
|
return "".join(chr(c[0]) for c in run["chars"])
|
|
|
|
|
|
def solid_regions(page):
|
|
"""Every solid filled region, taken as a whole. A blackout bar can be a
|
|
plain rectangle OR a filled four-sided path -- visually identical,
|
|
different in the PDF -- so we measure whole regions, never segments."""
|
|
return [d["rect"] for d in page.get_drawings() if d["type"] == "f"]
|
|
|
|
|
|
def vertical_rules(page):
|
|
"""x-positions of thin vertical lines: <1.2pt wide, >8pt tall, drawn
|
|
either as skinny rectangles or as literal line segments."""
|
|
xs = set()
|
|
for d in page.get_drawings():
|
|
for item in d["items"]:
|
|
if item[0] == "re" and item[1].width < 1.2 and item[1].height > 8:
|
|
xs.add(round(item[1].x0 + item[1].width / 2, 1))
|
|
elif item[0] == "l":
|
|
a, b = item[1], item[2]
|
|
if abs(a.x - b.x) < 1.2 and abs(a.y - b.y) > 8:
|
|
xs.add(round((a.x + b.x) / 2, 1))
|
|
return xs
|
|
|
|
|
|
def row_numbers(runs, furniture_font, table_left):
|
|
"""Excel's printed row numbers -> {vertical position: row number}.
|
|
Digits of one number are drawn separately, so group them by line and
|
|
read left to right."""
|
|
lines = {}
|
|
for run in runs:
|
|
if run["font"] != furniture_font:
|
|
continue
|
|
for ch in run["chars"]:
|
|
x, y = ch[2]
|
|
if x < table_left:
|
|
lines.setdefault(round(y, 1), []).append((x, chr(ch[0])))
|
|
out = {}
|
|
for y, chars in lines.items():
|
|
text = "".join(c for _, c in sorted(chars))
|
|
if text.isdigit():
|
|
out[y] = int(text)
|
|
return out
|