Python填充PDF表单字段值多余垂直空白问题解决方案
问题背景
我们当前使用如下工作流生成可填写PDF并通过Python完成内容填充,该流程依赖pdftk外部工具,现在需要移除该依赖:
- 在LibreOffice Writer(7.3.4.2版本)中生成带表单字段(controls控件)的.odt文件
- 将文件导出为PDF格式(LibreOffice可直接导出可填写PDF;如果使用Word则需要通过Acrobat后处理才能生成可填写PDF,我们没有配备Acrobat工具)
- 在Python中借助fdfgen库生成包含填充值的.fdf文件
- 在Python中调用外部pdftk工具将.fdf文件与可填写PDF合并,生成flattened(不可编辑)的最终PDF文件
我们的目标是完全不依赖pdftk或其他任何外部工具,移除上述步骤4(如果不再需要.fdf格式,也可以一并移除步骤3)。
已尝试方案及存在的问题
我们已经测试了pypdf2、pdfrw(实际使用pdfrw2)、fillpdf三个Python模块,均存在无法满足需求的问题:
- 使用pypdf2和pdfrw填充时,字段值的上方和/或下方会出现多余垂直空白。由于字段背景为不透明样式,该空白会挤占表单可用垂直空间、遮挡字段标签(为了展示问题我们将字段设置为灰色背景加边框,正常使用场景下字段为白色无边框样式)
- fillpdf无法识别多行文本字段,仅能生成被裁剪的单行结果
- 使用pdftk方案填充时不会出现上述多余垂直空白的问题
诉求
我们需要以下三类可行方案:
a)在LibreOffice导出PDF环节消除该多余垂直空白
b)通过纯Python方案消除填充后PDF中的多余垂直空白
c)将字段背景设置为透明,使得可以安全将整个字段上移,即使重叠也不会遮挡字段标签
效果对比
- LibreOffice中编辑的文档效果:默认值上方无垂直空白

- LibreOffice导出的可填写PDF效果:此时已出现多余垂直空白,初步猜测问题出在LibreOffice PDF导出环节,但使用pdftk方案填充时该空白并不会出现

- 使用pypdf2生成的填充后PDF效果(pdfrw生成效果与之一致):

- 使用现有工作流(fdfgen+外部调用pdftk)生成的填充后PDF效果:

参考代码
以下为生成各版本填充PDF所用的代码:
import json import os cluePdfName='filled.pdf' clueFdfName='filled.fdf' fillableClueReportPdfFileName='clueReportFillable.pdf' fields={ 'titleField':'SEARCH AND RESCUE\nSEARCH AND RESCUE\nSEARCH AND RESCUE SEARCH AND RESCUE SEARCH AND RESCUE ', 'incidentNameField':'incident', 'instructionsCollectField':True, 'instructionsOtherField':True, 'descriptionField':'long description long description long description long description long description long description long description long description long description long description long description long description long description', 'locationField':'long description long description long description long description long description long description long description long description long description long description long description long description long description', 'locationRadioGPSField':'(Radio GPS: 12345 67890)', 'instructionsOtherTextField':'do some stuff'} ################################################ # 部分1: pypdf2实现 ################################################ from PyPDF2 import PdfReader,PdfWriter from PyPDF2.generic import NameObject,TextStringObject,NumberObject,BooleanObject reader=PdfReader(fillableClueReportPdfFileName) writer=PdfWriter() page=reader.pages[0] pdfFields=reader.get_fields() writer.add_page(page) # 重写PdfWriter.update_page_form_field_values逻辑 # 参考实现来源:https://stackoverflow.com/a/48412434/3577105 # - 填充文本字段和布尔值(复选框'/Btn'字段) # - 将/AS设为相同值,解决字段值点击后才可见的问题 # - 最后为所有字段设置只读标记 for j in range(0, len(page['/Annots'])): writer_annot = page['/Annots'][j].getObject() for field in fields: if writer_annot.get('/T') == field: val=fields[field] valObj=TextStringObject('---') className=val.__class__.__name__ if className=='str': valObj=TextStringObject(val) elif className=='bool': # 复选框需要NameObject类型,值为/Yes或/Off if val: valObj=NameObject('/Yes') else: valObj=NameObject('/Off') elif className in ['int','float']: valObj=TextStringObject(str(val)) print('updating '+str(field)+' --> '+str(valObj)) writer_annot.update({ NameObject("/V"): valObj, }) ff=writer_annot.get('/Ff') if ff: newff=ff|1 # 为字段设置只读标记,不修改其他位配置 print('Ff: '+str(ff)+' --> '+str(newff)) writer_annot.update({NameObject('/Ff'): NumberObject(newff)}) else: writer_annot.update({NameObject('/Ff'): NumberObject(1)}) with open(cluePdfName,'wb') as out: writer.write(out) ################################################ # 部分2: fdfgen+pdftk实现 ################################################ from fdfgen import forge_fdf fdf=forge_fdf("",fields.items(),[],[],[]) fdf_file=open(clueFdfName,"wb") fdf_file.write(fdf) fdf_file.close() cluePdfTkName=cluePdfName.replace('.pdf','_pdftk.pdf') pdftk_cmd='pdftk "'+fillableClueReportPdfFileName+'" fill_form "'+clueFdfName+'" output "'+cluePdfTkName+'" flatten' print("Calling pdftk with the following command:") print(pdftk_cmd) os.system(pdftk_cmd) ################################################ # 部分3: pdfrw实现 # 参考实现来源:https://akdux.com/python/2020/10/31/python-fill-pdf-files.html # 执行pip install pdfrw2后,import pdfrw实际导入的是pdfrw2(v0.5版本) ################################################ import pdfrw ANNOT_KEY = '/Annots' ANNOT_FIELD_KEY = '/T' ANNOT_VAL_KEY = '/V' ANNOT_RECT_KEY = '/Rect' SUBTYPE_KEY = '/Subtype' WIDGET_SUBTYPE_KEY = '/Widget' def fill_pdf(input_pdf_path, output_pdf_path, data_dict): template_pdf = pdfrw.PdfReader(input_pdf_path) for page in template_pdf.pages: annotations = page[ANNOT_KEY] for annotation in annotations: if annotation[SUBTYPE_KEY] == WIDGET_SUBTYPE_KEY: if annotation[ANNOT_FIELD_KEY]: key = annotation[ANNOT_FIELD_KEY][1:-1] if key in data_dict.keys(): if type(data_dict[key]) == bool: if data_dict[key] == True: annotation.update(pdfrw.PdfDict( AS=pdfrw.PdfName('Yes'))) else: annotation.update( pdfrw.PdfDict(V='{}'.format(data_dict[key])) ) annotation.update(pdfrw.PdfDict(AP='')) template_pdf.Root.AcroForm.update(pdfrw.PdfDict(NeedAppearances=pdfrw.PdfObject('true'))) pdfrw.PdfWriter().write(output_pdf_path, template_pdf) fill_pdf(fillableClueReportPdfFileName,cluePdfName.replace('.pdf','_pdfrw.pdf'),fields) ################################################ # 部分4: fillpdf实现 ################################################ import fillpdf from fillpdf import fillpdfs fillpdfs.write_fillable_pdf(fillableClueReportPdfFileName,cluePdfName.replace('.pdf','_fillpdf.pdf'),fields,flatten=True)
内容的提问来源于stack exchange,提问作者Tom Grundy
相关产品推荐
相关产品推荐

