diffu_test / diffu /data /parse.py
Gabriel's picture
Diffu Studio ZeroGPU Space: app.py + vendored packages + aokit AoT + redesigned panel
333ff0e verified
Raw
History Blame Contribute Delete
4.17 kB
"""Parse PAGE-XML and ALTO-XML into per-line records. Namespace-agnostic (matches local tag names),
so it works across PAGE/ALTO versions. Pure stdlib — no torch needed.
"""
from __future__ import annotations
import os
import xml.etree.ElementTree as ET
from pydantic import BaseModel
class LineRecord(BaseModel):
image: str # page image filename as referenced in the XML
bbox: tuple[float, float, float, float] # (x0, y0, x1, y1)
text: str
source_id: str # page/document id (writer proxy)
polygon: list[tuple[float, float]] | None = None # PAGE <Coords> line polygon (None for ALTO rects)
def _local(tag: str) -> str:
return tag.rsplit("}", 1)[-1] # strip "{namespace}" -> local name
def _iter_local(elem: ET.Element, name: str) -> list[ET.Element]:
return [e for e in elem.iter() if _local(e.tag) == name]
def _parse_points(points: str) -> list[tuple[float, float]]:
"""Parse a PAGE ``points="x1,y1 x2,y2 …"`` string into a list of (x, y) vertices."""
pts: list[tuple[float, float]] = []
for pair in points.split():
if "," in pair:
x, y = pair.split(",")[:2]
pts.append((float(x), float(y)))
return pts
def _points_to_bbox(points: str) -> tuple[float, float, float, float] | None:
pts = _parse_points(points)
if not pts:
return None
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
return (min(xs), min(ys), max(xs), max(ys))
def _page_line_text(line: ET.Element) -> str:
# Prefer the TextLine-level TextEquiv/Unicode; else join Word-level Unicodes.
for te in line:
if _local(te.tag) == "TextEquiv":
uni = next((u for u in te if _local(u.tag) == "Unicode"), None)
if uni is not None and uni.text:
return uni.text
words: list[str] = []
for w in line:
if _local(w.tag) == "Word":
for te in w:
if _local(te.tag) == "TextEquiv":
uni = next((u for u in te if _local(u.tag) == "Unicode"), None)
if uni is not None and uni.text:
words.append(uni.text)
return " ".join(words)
def parse_page_xml(path: str) -> list[LineRecord]:
root = ET.parse(path).getroot()
records: list[LineRecord] = []
for page in _iter_local(root, "Page"):
img = page.attrib.get("imageFilename", "")
src = os.path.splitext(os.path.basename(img or path))[0]
for line in _iter_local(page, "TextLine"):
coords = next((e for e in line if _local(e.tag) == "Coords"), None)
pts = _parse_points(coords.attrib.get("points", "")) if coords is not None else []
if not pts:
continue
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
bbox = (min(xs), min(ys), max(xs), max(ys))
text = _page_line_text(line)
if text:
poly = pts if len(pts) >= 3 else None
records.append(LineRecord(image=img, bbox=bbox, text=text, source_id=src, polygon=poly))
return records
def parse_alto_xml(path: str) -> list[LineRecord]:
root = ET.parse(path).getroot()
fname = next((e.text for e in root.iter() if _local(e.tag) == "fileName" and e.text), "")
src = os.path.splitext(os.path.basename(fname or path))[0]
records: list[LineRecord] = []
for line in _iter_local(root, "TextLine"):
a = line.attrib
try:
x, y, w, h = float(a["HPOS"]), float(a["VPOS"]), float(a["WIDTH"]), float(a["HEIGHT"])
except KeyError:
continue
strings = [s.attrib.get("CONTENT", "") for s in line if _local(s.tag) == "String"]
text = " ".join(s for s in strings if s)
if text:
records.append(LineRecord(image=fname, bbox=(x, y, x + w, y + h), text=text, source_id=src))
return records
def parse_file(path: str) -> list[LineRecord]:
"""Auto-detect PAGE vs ALTO from the root tag."""
root = ET.parse(path).getroot()
if _local(root.tag).lower() == "alto":
return parse_alto_xml(path)
return parse_page_xml(path) # PcGts / PAGE