Files
pinion-pdf/pinion/page.py
T
daveandClaude Fable 5 3902ce043e pinion-pdf 1.0.0: rebuild a CSV from a spreadsheet printed to PDF
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
2026-09-07 00:34:28 -04:00

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