PDF-форма, заполненная PyPDF2, не отображается в печати

Мне нужно заполнить форму PDF в пакетном режиме, поэтому попытался написать код Python, чтобы сделать это для меня из CSV-файла. Я использовал второй ответ в этом вопросе, и он отлично заполняет формы, однако, когда я открываю заполненные формы, ответы не отображаются, если не выбрано соответствующее поле. Также ответы не отображаются при печати формы. Я заглянул в документы PyPDF2, чтобы выяснить, могу ли я сгладить сгенерированные формы, но эти функции еще не реализованы, хотя об этом просили около года назад. Я предпочитаю не использовать pdftk, поэтому я могу скомпилировать скрипт без необходимости большей зависимости. При использовании оригинального кода в упомянутом вопросе некоторые поля отображаются в печати, а некоторые нет, поэтому я не могу понять, как они работают. Любая помощь приветствуется.

Вот код

# -*- coding: utf-8 -*-

from collections import OrderedDict
from PyPDF2 import PdfFileWriter, PdfFileReader


def _getFields(obj, tree=None, retval=None, fileobj=None):
    """
    Extracts field data if this PDF contains interactive form fields.
    The *tree* and *retval* parameters are for recursive use.

    :param fileobj: A file object (usually a text file) to write
    a report to on all interactive form fields found.
    :return: A dictionary where each key is a field name, and each
    value is a :class:`Field<PyPDF2.generic.Field>` object. By
    default, the mapping name is used for keys.
    :rtype: dict, or ``None`` if form data could not be located.
    """
    fieldAttributes = {'/FT': 'Field Type', '/Parent': 'Parent', '/T': 'Field Name', '/TU': 'Alternate Field Name',
                   '/TM': 'Mapping Name', '/Ff': 'Field Flags', '/V': 'Value', '/DV': 'Default Value'}
    if retval is None:
        retval = {} #OrderedDict()
        catalog = obj.trailer["/Root"]
        # get the AcroForm tree
        if "/AcroForm" in catalog:
            tree = catalog["/AcroForm"]
        else:
            return None
    if tree is None:
        return retval

    obj._checkKids(tree, retval, fileobj)
    for attr in fieldAttributes:
        if attr in tree:
            # Tree is a field
            obj._buildField(tree, retval, fileobj, fieldAttributes)
            break

    if "/Fields" in tree:
        fields = tree["/Fields"]
        for f in fields:
            field = f.getObject()
            obj._buildField(field, retval, fileobj, fieldAttributes)

    return retval


def get_form_fields(infile):
    infile = PdfFileReader(open(infile, 'rb'))
    fields = _getFields(infile)
    return {k: v.get('/V', '') for k, v in fields.items()}


def update_form_values(infile, outfile, newvals=None):
    pdf = PdfFileReader(open(infile, 'rb'))
    writer = PdfFileWriter()

    for i in range(pdf.getNumPages()):
        page = pdf.getPage(i)
        try:
            if newvals:
                writer.updatePageFormFieldValues(page, newvals)
            else:
                writer.updatePageFormFieldValues(page,
                                             {k: f'#{i} {k}={v}'
                                              for i, (k, v) in 
enumerate(get_form_fields(infile).items())
                                              })
            writer.addPage(page)
        except Exception as e:
            print(repr(e))
            writer.addPage(page)

    with open(outfile, 'wb') as out:
        writer.write(out)


if __name__ == '__main__':
    import csv    
    import os
    from glob import glob
    cwd=os.getcwd()
    outdir=os.path.join(cwd,'output')
    csv_file_name=os.path.join(cwd,'formData.csv')
    pdf_file_name=glob(os.path.join(cwd,'*.pdf'))[0]
    if not pdf_file_name:
        print('No pdf file found')
    if not os.path.isdir(outdir):
        os.mkdir(outdir)
    if not os.path.isfile(csv_file_name):
        fields=get_form_fields(pdf_file_name)
        with open(csv_file_name,'w',newline='') as csv_file:
            csvwriter=csv.writer(csv_file,delimiter=',')
            csvwriter.writerow(['user label'])
            csvwriter.writerow(['fields']+list(fields.keys()))
            csvwriter.writerow(['Mr. X']+list(fields.values()))
    else:
        with open(csv_file_name,'r',newline='') as csv_file:
            csvreader=csv.reader(csv_file,delimiter=',')
            csvdata=list(csvreader)
        fields=csvdata[1][1:]
        for frmi in csvdata[2:]:
            frmdict=dict(zip(fields,frmi[1:]))
            outfile=os.path.join(outdir,frmi[0]+'.pdf')
            update_form_values(pdf_file_name, outfile,frmdict)

5 ответов

Решение

У меня была та же проблема, и, по-видимому, добавление атрибута /NeedsAppearance в объект PdfWriter AcroForm устранило проблему (см. https://github.com/mstamy2/PyPDF2/issues/355). С большой помощью ademidun ( https://github.com/ademidun) я смог заполнить PDF-форму и правильно отобразить значения полей. Ниже приведен пример:

from PyPDF2 import PdfFileWriter, PdfFileReader
from PyPDF2.generic import BooleanObject, NameObject, IndirectObject

def set_need_appearances_writer(writer):
    # See 12.7.2 and 7.7.2 for more information:
    # http://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
    try:
        catalog = writer._root_object
        # get the AcroForm tree and add "/NeedAppearances attribute
        if "/AcroForm" not in catalog:
            writer._root_object.update({
                NameObject("/AcroForm"): IndirectObject(len(writer._objects), 0, writer)})

        need_appearances = NameObject("/NeedAppearances")
        writer._root_object["/AcroForm"][need_appearances] = BooleanObject(True)
        return writer

    except Exception as e:
        print('set_need_appearances_writer() catch : ', repr(e))
        return writer

infile = "myInputPdf.pdf"
outfile = "myOutputPdf.pdf"

inputStream = open(infile, "rb")
pdf_reader = PdfFileReader(inputStream, strict=False)
if "/AcroForm" in pdf_reader.trailer["/Root"]:
    pdf_reader.trailer["/Root"]["/AcroForm"].update(
        {NameObject("/NeedAppearances"): BooleanObject(True)})

pdf_writer = PdfFileWriter()
set_need_appearances_writer(pdf_writer)
if "/AcroForm" in pdf_writer._root_object:
    pdf_writer._root_object["/AcroForm"].update(
        {NameObject("/NeedAppearances"): BooleanObject(True)})

field_dictionary = {"Field1": "Value1", "Field2": "Value2"}

pdf_writer.addPage(pdf_reader.getPage(0))
pdf_writer.updatePageFormFieldValues(pdf_writer.getPage(0), field_dictionary)

outputStream = open(outfile, "wb")
pdf_writer.write(outputStream)

inputStream.close()
outputStream.close()

Основная причина, по которой поля формы не отображаются после заполнения, заключается в том, что значения не добавляются в поток. Добавление «NeedAppearances» сообщает программе чтения PDF, что ему необходимо обновить внешний вид, в этом случае ему необходимо создать поток для каждого значения поля, но не все программы чтения PDF будут учитывать это, и поля могут по-прежнему выглядеть пустыми или иметь значения по умолчанию. ценности.

Лучшее решение, чтобы убедиться, что поля обновлены для любого читателя, — это создать поток для каждого поля и добавить его в XObject поля.

Вот пример решения для однострочных текстовых полей. Он также кодирует поток, обновляет значение по умолчанию и устанавливает поля только для чтения, которые являются необязательными.

      # Example data.
data = {
    "field_name": "some value"
}

# Get template.
template = PdfReader("template-form.pdf", strict=False)

# Initialize writer.
writer = PdfWriter()

# Add the template page.
writer.add_page(template.pages[0])

# Get page annotations.
page_annotations = writer.pages[0][PageAttributes.ANNOTS]

# Loop through page annotations (fields).
for index in range(len(page_annotations)):  # type: ignore
    # Get annotation object.
    annotation = page_annotations[index].get_object()  # type: ignore

    # Get existing values needed to create the new stream and update the field.
    field = annotation.get(NameObject("/T"))
    new_value = data.get(field, 'N/A')
    ap = annotation.get(AnnotationDictionaryAttributes.AP)
    x_object = ap.get(NameObject("/N")).get_object()
    font = annotation.get(InteractiveFormDictEntries.DA)
    rect = annotation.get(AnnotationDictionaryAttributes.Rect)

    # Calculate the text position.
    font_size = float(font.split(" ")[1])
    w = round(float(rect[2] - rect[0] - 2), 2)
    h = round(float(rect[3] - rect[1] - 2), 2)
    text_position_h = h / 2 - font_size / 3  # approximation

    # Create a new XObject stream.
    new_stream = f'''
        /Tx BMC 
        q
        1 1 {w} {h} re W n
        BT
        {font}
        2 {text_position_h} Td
        ({new_value}) Tj
        ET
        Q
        EMC
    '''

    # Add Filter type to XObject.
    x_object.update(
        {
            NameObject(StreamAttributes.FILTER): NameObject(FilterTypes.FLATE_DECODE)
        }
    )

    # Update and encode XObject stream.
    x_object._data = FlateDecode.encode(encode_pdfdocencoding(new_stream))

    # Update annotation dictionary.
    annotation.update(
        {
            # Update Value.
            NameObject(FieldDictionaryAttributes.V): TextStringObject(
                new_value
            ),
            # Update Default Value.
            NameObject(FieldDictionaryAttributes.DV): TextStringObject(
                new_value
            ),
            # Set Read Only flag.
            NameObject(FieldDictionaryAttributes.Ff): NumberObject(
                FieldFlag(1)
            )
        }
    )

# Clone document root & metadata from template.
# This is required so that the document doesn't try to save before closing.
writer.clone_reader_document_root(template)

# write "output".
with open(f"output.pdf", "wb") as output_stream:
    writer.write(output_stream)  # type: ignore

Спасибо fidoriel и другим участникам обсуждения здесь: https://github.com/py-pdf/PyPDF2/issues/355.

Вот что у меня работает на Python 3.8 и PyPDF4 (но я думаю, что это будет работать и с PyPDF2):

      #!/usr/bin/env python3
from PyPDF4.generic import NameObject
from PyPDF4.generic import TextStringObject
from PyPDF4.pdf import PdfFileReader
from PyPDF4.pdf import PdfFileWriter

import random
import sys

reader = PdfFileReader(sys.argv[1])

writer = PdfFileWriter()
# Try to "clone" the original one (note the library has cloneDocumentFromReader)
# but the render pdf is blank
writer.appendPagesFromReader(reader)
writer._info = reader.trailer["/Info"]
reader_trailer = reader.trailer["/Root"]
writer._root_object.update(
    {
        key: reader_trailer[key]
        for key in reader_trailer
        if key in ("/AcroForm", "/Lang", "/MarkInfo")
    }
)

page = writer.getPage(0)

params = {"Foo": "Bar"}

# Inspired by updatePageFormFieldValues but also handle checkboxes
for annot in page["/Annots"]:
    writer_annot = annot.getObject()
    field = writer_annot["/T"]
    if writer_annot["/FT"] == "/Btn":
        value = params.get(field, random.getrandbits(1))
        if value:
            writer_annot.update(
                {
                    NameObject("/AS"): NameObject("/On"),
                    NameObject("/V"): NameObject("/On"),
                }
            )
    elif writer_annot["/FT"] == "/Tx":
        value = params.get(field, field)
        writer_annot.update(
            {
                NameObject("/V"): TextStringObject(value),
            }
        )

with open(sys.argv[2], "wb") as f:
    writer.write(f)

Это обновляет текстовые поля и флажки.

Я считаю, что ключевой момент копирует некоторые части из исходного файла:

      reader_trailer = reader.trailer["/Root"]
writer._root_object.update(
    {
        key: reader_trailer[key]
        for key in reader_trailer
        if key in ("/AcroForm", "/Lang", "/MarkInfo")
    }
)

Примечание: не стесняйтесь делиться этим решением в других местах, я проконсультировался с множеством SO-вопросов, связанных с этой темой.

Что сработало для меня, так это снова открыть сpdfrw

Для Adobe Reader, Acrobat, Skim и Mac OS Preview мне помогло следующее:

      pip install pdfrw
      import pdfrw

pdf = pdfrw.PdfReader("<input_name>")
for page in pdf.pages:
    annotations = page.get("/Annots")
    if annotations:
        for annotation in annotations:
            annotation.update(pdfrw.PdfDict(AP=""))
                        
pdf.Root.AcroForm.update(pdfrw.PdfDict(NeedAppearances=pdfrw.PdfObject('true')))
pdfrw.PdfWriter().write("<output_name>", pdf)

ответ алеписа был ближе всего к работе для меня (спасибо, алеписа), но мне просто нужно было изменить один небольшой раздел

        elif writer_annot["/FT"] == "/Tx":
    value = params.get(field, field)
    writer_annot.update(

Это приводило к выводу, что в моем PDF-файле нужные поля были обновлены на основе словаря с именами полей и значениями, которые я ему передал, но каждое заполняемое поле, независимо от того, хотел я, чтобы оно было заполнено или нет, было заполнено именем этого заполняемого поля. Я изменил оператор elif на приведенный ниже, и все заработало как по маслу!

      elif writer_annot["/FT"] == "/Tx":
    field_value = field_values.get(field_name, "")
    writer_annot.update({NameObject("/V"): TextStringObject(field_value),
                        #This line below is just for formatting
                        NameObject("/DA"): TextStringObject("/Helv 0 Tf 0 g")})

Это, вложенное обратно в остальную часть сценария alepisa, должно работать для всех, у кого есть проблемы с получением вывода в Acrobat, чтобы отображать значения без нажатия на ячейку!

Другие вопросы по тегам