2025, Oct 17 14:18

Извлечение координат полей XFA в PDF с PyMuPDF и pikepdf

Как извлечь координаты полей XFA в PDF: почему PageItemUIDToLocationDataMap пропускает элементы и как найти белые прямоугольники в PyMuPDF и pikepdf.

Извлечение координат полей формы из PDF с XFA (Adobe) обычно кажется простой задачей — до тех пор, пока часть полей упорно не исчезает из собранных данных. Часто выбирают путь чтения /PageItemUIDToLocationDataMap в /PieceInfo, но в некоторых файлах отображается лишь малая часть полей. Значит, цель — вернуть недостающие координаты, не прибегая к коммерческим инструментам.

Постановка задачи

Подход ниже проходит по страницам, извлекает /PageItemUIDToLocationDataMap из /PieceInfo[/InDesign], пишет результаты в CSV и ставит метки в PDF. Он наглядно демонстрирует проблему: на отдельных страницах карта пропускает поля, которые отчётливо видны при открытии документа в просмотрщике.

import pikepdf
import fitz  # PyMuPDF
import csv

SRC_PDF = "input.pdf"
CSV_OUT = "points.csv"
PDF_OUT = "output.pdf"
MAP_KEY = "/PageItemUIDToLocationDataMap"

def pull_datamap_points(pdf_file, target_key=MAP_KEY):
    rows_out = []
    with pikepdf.open(pdf_file) as pdf:
        for idx, pg in enumerate(pdf.pages):
            piece_meta = pg.get('/PieceInfo', None)
            if piece_meta and '/InDesign' in piece_meta:
                idn = piece_meta['/InDesign']
                if target_key in idn:
                    for key, val in idn[target_key].items():
                        try:
                            uid = int(str(key).lstrip('/'))
                            kind_val = float(val[2])
                            crds = [float(n) for n in list(val)[3:7]]
                            rows_out.append([idx + 1, uid, kind_val] + crds)
                        except Exception as err:
                            print(f"Error parsing {key}:{val} ({err})")
    return rows_out

def count_pages(pdf_file):
    with pikepdf.open(pdf_file) as pdf:
        return len(pdf.pages)

def normalize_rows(data_rows, max_pages):
    Y_BASE = 420.945  # Локальная константа для преобразования координаты y

    pg_total = count_pages(SRC_PDF)
    map_page = lambda p: 2 if (p >= max_pages) else (p + 1 if p > 1 else p)
    norm_rows = []
    for rec in data_rows:
        pg, uid, kind, x0, y0, x1_, y1_ = rec
        pg_fix = map_page(pg)
        y0n = round(Y_BASE - y0, 3)
        y1n = round(Y_BASE - y1_, 3)
        x0n = round(x0, 3)
        x1n = round(x1_, 3)
        height = round(abs(y1n - y0n), 1)
        norm_rows.append([pg_fix, uid, kind, x0n, y0n, x1n, y1n, height])
    return norm_rows

def order_and_pick(data_rows):
    rows_sorted = sorted(data_rows, key=lambda r: (r[0], -r[6], r[3], r[1]))
    picked = []
    for rec in rows_sorted:
        if (rec[2] == 4 and rec[7] == 17):
            picked.append(rec)
    return picked

def export_csv(csv_path, data_rows):
    with open(csv_path, 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f)
        w.writerow(['page', 'id', 'type', 'x1', 'y1', 'x2', 'y2', 'h'])
        w.writerows(data_rows)

def paint_points(src_pdf, out_pdf, data_rows):
    doc = fitz.open(src_pdf)
    for rec in data_rows:
        pno = int(rec[0])
        cx = rec[3]
        cy = rec[6]
        page = doc[pno - 1]
        y_mupdf = page.rect.height - cy
        page.draw_circle((cx, y_mupdf), radius=2, color=(0, 0, 0), fill=(0, 0, 0))
    doc.save(out_pdf)

if __name__ == "__main__":
    raw_points = pull_datamap_points(SRC_PDF)
    pg_count = count_pages(SRC_PDF)
    mapped_points = normalize_rows(raw_points, pg_count)
    final_points = order_and_pick(mapped_points)
    export_csv(CSV_OUT, final_points)
    paint_points(SRC_PDF, PDF_OUT, final_points)
    print(f"Done. Points: {len(final_points)}; Wrote {CSV_OUT} and {PDF_OUT}")

Если нужно изучить всё, что обнаружено на странице до какой-либо фильтрации, пропустите шаг фильтрации по типу/высоте и экспортируйте все строки в CSV.

Что происходит на самом деле

/PageItemUIDToLocationDataMap полезен, но он не гарантирует, что на всех страницах будут перечислены каждое видимое поле ввода. В примере страницы, которые в просмотрщике явно заполняемые, при опоре только на эту карту всё равно не возвращают полный набор записей. При этом недостающие поля визуально присутствуют на странице как фигуры, что даёт другой, надёжный способ их найти.

Если сместить задачу с «прочитать все XFA‑поля из карты» на «найти белые области ввода, присутствующие на странице», пропуски исчезают. Прямоугольники можно определить напрямую из инструкций отрисовки страницы, выбирая закрашенные белым цветом.

Решение: определять белые прямоугольники по операциям рисования

Проверка операций рисования на странице и фильтрация бело-залитых прямоугольников позволяет вернуть недостающие координаты и обойтись без проприетарных инструментов. Код ниже записывает координаты прямоугольников в CSV и помечает каждый из них небольшим кружком на странице.

import fitz  # PyMuPDF
import csv

SRC_DOC = "input.pdf"
MARKED_PDF = "output.pdf"
BOXES_CSV = "output.csv"

def is_color(color, target=(1, 1, 1)):
    return color == target

pdf = fitz.open(SRC_DOC)

# Номера страниц с нуля; при необходимости измените или используйте range(len(pdf)) для обработки всех
pages_idx = [1]

with open(BOXES_CSV, mode="w", newline="", encoding="utf-8") as fh:
    w = csv.writer(fh)
    w.writerow(["page_num", "x0", "y0", "x1", "y1"])
    for idx in pages_idx:
        pg = pdf[idx]
        vector_ops = pg.get_drawings()
        canvas = pg.new_shape()
        for op in vector_ops:
            rc = op.get("rect")
            fc = op.get("fill")
            if rc and is_color(fc, target=(1, 1, 1)):
                x0, y0, x1, y1 = rc
                cx, cy = x0, y1
                canvas.draw_circle((cx, cy), 2)
                w.writerow([idx, x0, y0, x1, y1])
        canvas.finish(color=(0, 0, 1), fill=None)
        canvas.commit()

pdf.save(MARKED_PDF)
pdf.close()

Этот подход эффективен, потому что искомые области представлены белыми прямоугольниками в потоке содержимого страницы. Собирая операции рисования и проверяя цвет заливки (1, 1, 1), можно получить их координаты для последующей автоматизации.

Почему это важно

Поиск полей в сложных PDF можно вести разными путями. Если метаданные дают лишь частичный результат, прямой анализ инструкций рисования страницы — практичный запасной вариант. Получив границы коробок, вы можете размещать собственные поля формы и автоматизировать ввод данных, оставаясь полностью в пределах open‑source инструментов.

Выводы

Когда /PageItemUIDToLocationDataMap не перечисляет все поля, относитесь к задаче как к поиску визуальных полей ввода, а не к извлечению XFA‑карт. Получайте примитивы рисования с PyMuPDF, фильтруйте бело-залитые прямоугольники и фиксируйте их координаты. На этапе исследования выгружайте CSV без фильтров, а затем добавляйте нужные ограничения по типам или размерам под вашу верстку. Так вы получите надёжный, не зависящий от проприетарного ПО конвейер для восстановления нужных позиций и последующей автозаполняемости.

Материал основан на вопросе на StackOverflow от flywire и ответе flywire.