【问题标题】:PDFBox: How to "flatten" a PDF-form?PDFBox:如何“展平” PDF 表单?
【发布时间】:2013-01-05 10:29:42
【问题描述】:

如何使用 PDFBox “展平” PDF 表单(删除表单域但保留该域的文本)?

Same question was answered here:

执行此操作的快速方法是从 acrofrom 中删除字段。

为此,您只需要获取文档目录,然后获取 acroform 然后从此 acroform 中删除所有字段。

图形表示与注释链接并留在 文件。

所以我写了这段代码:

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;

public class PdfBoxTest {
    public void test() throws Exception {
        PDDocument pdDoc = PDDocument.load(new File("E:\\Form-Test.pdf"));
        PDDocumentCatalog pdCatalog = pdDoc.getDocumentCatalog();
        PDAcroForm acroForm = pdCatalog.getAcroForm();

        if (acroForm == null) {
            System.out.println("No form-field --> stop");
            return;
        }

        @SuppressWarnings("unchecked")
        List<PDField> fields = acroForm.getFields();

        // set the text in the form-field <-- does work
        for (PDField field : fields) {
            if (field.getFullyQualifiedName().equals("formfield1")) {
                field.setValue("Test-String");
            }
        }

        // remove form-field but keep text ???
        // acroForm.getFields().clear();         <-- does not work
        // acroForm.setFields(null);             <-- does not work
        // acroForm.setFields(new ArrayList());  <-- does not work
        // ???

        pdDoc.save("E:\\Form-Test-Result.pdf");
        pdDoc.close();
    }
}

【问题讨论】:

  • 代码的结果是什么?
  • @RachelGallen:结果是表单字段中带有“Test-String”的 PDF。但是表单域仍然存在并且可以编辑。
  • @Rachel:谢谢,但我必须自动完成,我用 PDFBox 搜索解决方案。但是我会仔细阅读文章,也许有一些有用的东西。
  • 如果我将其作为答案发布,您会投票并接受吗?

标签: java pdfbox pdf-form


【解决方案1】:

这是 Thomas 的答案,来自 PDFBox-Mailinglist:

您需要通过 COSDictionary 获取字段。尝试这个 代码...

PDDocument pdDoc = PDDocument.load(new File("E:\\Form-Test.pdf"));
PDDocumentCatalog pdCatalog = pdDoc.getDocumentCatalog();
PDAcroForm acroForm = pdCatalog.getAcroForm();

COSDictionary acroFormDict = acroForm.getDictionary();
COSArray fields = acroFormDict.getDictionaryObject("Fields");
fields.clear();

【讨论】:

  • 代码对我不起作用。执行此代码后,PDFBox 不再识别表单域,但 AcrobatPdfReader 仍显示表单域。 (也许其他一些部分必须从 COSDictionary 中删除,我不知道。)但是我发布了答案,因为它可能有助于找到正确的答案。
  • 它不起作用,因为注释小部件可以独立于表单字段而存在。删除字段时,不会删除小部件,即使它们不属于任何表单域,它们也会保留在那里。为了有效,您必须从每个页面中删除小部件的注释;为了提高效率,您也应该从文档中删除小部件对象(这意味着删除不再引用的部分)。
【解决方案2】:

setReadOnly 确实为我工作,如下所示 -

   @SuppressWarnings("unchecked")
    List<PDField> fields = acroForm.getFields();
    for (PDField field : fields) {
        if (field.getFullyQualifiedName().equals("formfield1")) {
            field.setReadOnly(true);
        }
    }

【讨论】:

    【解决方案3】:

    这确实有效 - 我遇到了这个问题,调试了一整夜,但最终想出了如何做到这一点:)

    假设您有能力以某种方式编辑 PDF/对 PDF 有一定的控制权。

    首先,使用 Acrobat Pro 编辑表单。将它们设为隐藏和只读。

    然后你需要使用两个库:PDFBox 和 PDFClown。

    PDFBox 删除了告诉 Adob​​e Reader 它是一个表单的东西; PDFClown 删除实际字段。必须先完成 PDFClown,然后是 PDFBox(按此顺序。反之则行不通)。

    单字段示例代码:

    // PDF Clown code
    File file = new File("Some file path"); 
    Document document = file.getDocument();
    Form form = file.getDocument.getForm();
    Fields fields = form.getFields();
    Field field = fields.get("some_field_name");
    
    PageStamper stamper = new PageStamper(); 
    FieldWidgets widgets = field.getWidgets();
    Widget widget = widgets.get(0); // Generally is 0.. experiment to figure out
    stamper.setPage(widget.getPage());
    
    // Write text using text form field position as pivot.
    PrimitiveComposer composer = stamper.getForeground();
    Font font = font.get(document, "some_path"); 
    composer.setFont(font, 10); 
    double xCoordinate = widget.getBox().getX();
    double yCoordinate = widget.getBox().getY(); 
    composer.showText("text i want to display", new Point2D.Double(xCoordinate, yCoordinate)); 
    
    // Actually delete the form field!
    field.delete();
    stamper.flush(); 
    
    // Create new buffer to output to... 
    Buffer buffer = new Buffer();
    file.save(buffer, SerializationModeEnum.Standard); 
    byte[] bytes = buffer.toByteArray(); 
    
    // PDFBox code
    InputStream pdfInput = new ByteArrayInputStream(bytes);
    PDDocument pdfDocument = PDDocument.load(pdfInput);
    
    // Tell Adobe we don't have forms anymore.
    PDDocumentCatalog pdCatalog = pdfDocument.getDocumentCatalog();
    PDAcroForm acroForm = pdCatalog.getAcroForm();
    COSDictionary acroFormDict = acroForm.getDictionary();
    COSArray cosFields = (COSArray) acroFormDict.getDictionaryObject("Fields");
    cosFields.clear();
    
    // Phew. Finally.
    pdfDocument.save("Some file path");
    

    这里和那里可能有一些错别字,但这应该足以了解要点:)

    【讨论】:

    • 如果你想知道许可证,PDFClown 是 LGPLv3,所以如果你正在开发服务器端的东西,它应该没问题(不是合法的建议..)。而PDFBox是Apache 2什么的,等于免费。
    • 非常感谢示例代码。它对我有用!
    • @JakeW 我很高兴地向您宣布,PDF Clown 从 0.2.0(和 0.1.2.1)版本开始原生支持表单展平。更多信息在这里:PDF Clown 0.2.0 — Enhanced content handling
    【解决方案4】:

    这是我在综合了我能找到的关于该主题的所有答案后得出的代码。这处理扁平化文本框、组合、列表、复选框和单选:

    public static void flattenPDF (PDDocument doc) throws IOException {
    
        //
        //  find the fields and their kids (widgets) on the input document
        //  (each child widget represents an appearance of the field data on the page, there may be multiple appearances)
        //
        PDDocumentCatalog catalog = doc.getDocumentCatalog();
        PDAcroForm form = catalog.getAcroForm();
        List<PDField> tmpfields = form.getFields();
        PDResources formresources = form.getDefaultResources();
        Map formfonts = formresources.getFonts();
        PDAnnotation ann;
    
        //
        // for each input document page convert the field annotations on the page into
        // content stream
        //
        List<PDPage> pages = catalog.getAllPages();
        Iterator<PDPage> pageiterator = pages.iterator();
        while (pageiterator.hasNext()) {
            //
            // get next page from input document
            //
            PDPage page = pageiterator.next();
    
            //
            // add the fonts from the input form to this pages resources
            // so the field values will display in the proper font
            //
            PDResources pageResources = page.getResources();
            Map pageFonts = pageResources.getFonts();
            pageFonts.putAll(formfonts);
            pageResources.setFonts(pageFonts);
    
            //
            // Create a content stream for the page for appending
            //
            PDPageContentStream contentStream = new PDPageContentStream(doc, page, true, true);
    
            //
            // Find the appearance widgets for all fields on the input page and insert them into content stream of the page
            //
            for (PDField tmpfield : tmpfields) {
                List widgets = tmpfield.getKids();
                if(widgets == null) {
                    widgets = new ArrayList();
                    widgets.add(tmpfield.getWidget());
                }
                Iterator<COSObjectable> widgetiterator = widgets.iterator();
                while (widgetiterator.hasNext()) {
                    COSObjectable next = widgetiterator.next();
                    if (next instanceof PDField) {
                        PDField foundfield = (PDField) next;
                        ann = foundfield.getWidget();
                    } else {
                        ann = (PDAnnotation) next;
                    }
                    if (ann.getPage().equals(page)) {
                        COSDictionary dict = ann.getDictionary();
                        if (dict != null) {
                            if(tmpfield instanceof PDVariableText || tmpfield instanceof PDPushButton) {
                                COSDictionary ap = (COSDictionary) dict.getDictionaryObject("AP");
                                if (ap != null) {
    
                                    contentStream.appendRawCommands("q\n");
                                    COSArray rectarray = (COSArray) dict.getDictionaryObject("Rect");
                                    if (rectarray != null) {
                                        float[] rect = rectarray.toFloatArray();
                                        String s = " 1 0 0 1  " + Float.toString(rect[0]) + " " + Float.toString(rect[1]) + " cm\n";
    
                                        contentStream.appendRawCommands(s);
                                    }
                                    COSStream stream = (COSStream) ap.getDictionaryObject("N");
                                    if (stream != null) {
                                        InputStream ioStream = stream.getUnfilteredStream();
                                        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
                                        byte[] buffer = new byte[4096];
                                        int amountRead = 0;
                                        while ((amountRead = ioStream.read(buffer, 0, buffer.length)) != -1) {
                                            byteArray.write(buffer, 0, amountRead);
                                        }
    
                                        contentStream.appendRawCommands(byteArray.toString() + "\n");
                                    }
    
                                    contentStream.appendRawCommands("Q\n");
                                }
                            } else if (tmpfield instanceof PDChoiceButton) {
                                COSDictionary ap = (COSDictionary) dict.getDictionaryObject("AP");
                                if(ap != null) {
                                    contentStream.appendRawCommands("q\n");
                                    COSArray rectarray = (COSArray) dict.getDictionaryObject("Rect");
                                    if (rectarray != null) {
                                        float[] rect = rectarray.toFloatArray();
                                        String s = " 1 0 0 1  " + Float.toString(rect[0]) + " " + Float.toString(rect[1]) + " cm\n";
    
                                        contentStream.appendRawCommands(s);
                                    }
    
                                    COSName cbValue = (COSName) dict.getDictionaryObject(COSName.AS);
                                    COSDictionary d = (COSDictionary) ap.getDictionaryObject(COSName.D);
                                    if (d != null) {
                                        COSStream stream = (COSStream) d.getDictionaryObject(cbValue);
                                        if(stream != null) {
                                            InputStream ioStream = stream.getUnfilteredStream();
                                            ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
                                            byte[] buffer = new byte[4096];
                                            int amountRead = 0;
                                            while ((amountRead = ioStream.read(buffer, 0, buffer.length)) != -1) {
                                                byteArray.write(buffer, 0, amountRead);
                                            }
    
                                            if (!(tmpfield instanceof PDCheckbox)){
                                                contentStream.appendRawCommands(byteArray.toString() + "\n");
                                            }
                                        }
                                    }
    
                                    COSDictionary n = (COSDictionary) ap.getDictionaryObject(COSName.N);
                                    if (n != null) {
                                        COSStream stream = (COSStream) n.getDictionaryObject(cbValue);
                                        if(stream != null) {
                                            InputStream ioStream = stream.getUnfilteredStream();
                                            ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
                                            byte[] buffer = new byte[4096];
                                            int amountRead = 0;
                                            while ((amountRead = ioStream.read(buffer, 0, buffer.length)) != -1) {
                                                byteArray.write(buffer, 0, amountRead);
                                            }
    
                                            contentStream.appendRawCommands(byteArray.toString() + "\n");
                                        }
                                    }
    
                                    contentStream.appendRawCommands("Q\n");
                                }
                            }
                        }
                    }
                }
            }
    
            // delete any field widget annotations and write it all to the page
            // leave other annotations on the page
            COSArrayList newanns = new COSArrayList();
            List anns = page.getAnnotations();
            ListIterator annotiterator = anns.listIterator();
            while (annotiterator.hasNext()) {
                COSObjectable next = (COSObjectable) annotiterator.next();
                if (!(next instanceof PDAnnotationWidget)) {
                    newanns.add(next);
                }
            }
    
            page.setAnnotations(newanns);
            contentStream.close();
        }
    
        //
        // Delete all fields from the form and their widgets (kids)
        //
        for (PDField tmpfield : tmpfields) {
            List kids = tmpfield.getKids();
            if(kids != null) kids.clear();
        }
    
        tmpfields.clear();
    
        // Tell Adobe we don't have forms anymore.
        PDDocumentCatalog pdCatalog = doc.getDocumentCatalog();
        PDAcroForm acroForm = pdCatalog.getAcroForm();
        COSDictionary acroFormDict = acroForm.getDictionary();
        COSArray cosFields = (COSArray) acroFormDict.getDictionaryObject("Fields");
        cosFields.clear();
    }
    

    这里的全班: https://gist.github.com/jribble/beddf7620536939f88db

    【讨论】:

    • 虽然您不应该只发布链接,但我确实认为该要点的代码太长,无法在此处发布。也许只是粘贴重要部分并提供链接作为完整代码。
    • 我使用了这段代码,但由于某种原因,我的复选框变成了灰色。任何线索为什么?
    • @Qedrix 我更新了代码以修复复选框上的灰色背景绘制。 tmpfield instanceof PDChoiceButton 中的第一个 byteArray 负责。
    【解决方案5】:

    要真正“扁平化”一个 acrobat 表单域,似乎要做的事情远比乍一看要多得多。 在检查了PDF standard 之后,我设法通过三个步骤实现了真正的扁平化:

    1. 保存字段值
    2. 删除小部件
    3. 删除表单域

    这三个步骤都可以用 pdfbox 完成(我用的是 1.8.5)。下面我将概述我是如何做到的。 PDF Debugger 是一个非常有用的工具,可以帮助您了解正在发生的事情。

    保存字段

    这是三步中最复杂的一步。

    为了保存字段的值,您必须将其内容保存到 pdf 的内容中对于每个字段的小部件。最简单的方法是将每个小部件的外观绘制到小部件的页面。

    void saveFieldValue( PDField field ) throws IOException
    {
        PDDocument document = getDocument( field );
        // see PDField.getWidget()
        for( PDAnnotationWidget widget : getWidgets( field ) )
        {
            PDPage parentPage = getPage( widget );
    
            try (PDPageContentStream contentStream = new PDPageContentStream( document, parentPage, true, true ))
            {
                writeContent( contentStream, widget );
            }
        }
    }
    
    void writeContent( PDPageContentStream contentStream, PDAnnotationWidget widget )
            throws IOException
    {
        PDAppearanceStream appearanceStream = getAppearanceStream( widget );
        PDXObject xobject = new PDXObjectForm( appearanceStream.getStream() );
        AffineTransform transformation = getPositioningTransformation( widget.getRectangle() );
    
        contentStream.drawXObject( xobject, transformation );
    }
    

    外观是一个包含所有小部件内容(值、字体、大小、旋转等)的 XObject 流。您只需将其放置在页面上您可以从小部件的矩形中提取的正确位置。

    删除小部件

    如上所述,每个字段可能有多个小部件。小部件负责如何编辑、触发、在不编辑时显示表单字段等。

    要删除一个,您必须将其从其页面的注释中删除。

    void removeWidget( PDAnnotationWidget widget ) throws IOException
    {
        PDPage widgetPage = getPage( widget );
        List<PDAnnotation> annotations = widgetPage.getAnnotations();
        PDAnnotation deleteCandidate = getMatchingCOSObjectable( annotations, widget );
        if( deleteCandidate != null && annotations.remove( deleteCandidate ) )
            widgetPage.setAnnotations( annotations );
    }
    

    请注意,注释可能不包含确切的 PDAnnotationWidget,因为它是一种包装器。您必须删除与 COSObject 匹配的那个。

    删除表单域

    作为最后一步,您删除表单域本身。这与上面的其他帖子没有太大区别。

    void removeFormfield( PDField field ) throws IOException
    {
        PDAcroForm acroForm = field.getAcroForm();
        List<PDField> acroFields = acroForm.getFields();
        List<PDField> removeCandidates = getFields( acroFields, field.getPartialName() );
        if( removeAll( acroFields, removeCandidates ) )
            acroForm.setFields( acroFields );
    }
    

    请注意,我在这里使用了自定义 removeAll 方法,因为 removeCandidates.removeAll() 没有按预期工作。

    对不起,我不能在这里提供所有的代码,但是有了上面的你应该可以自己写了。

    【讨论】:

    • 这会经常工作,但并非总是如此:在设置了 NeedsAppearances 标志的文档中,某些字段可能还没有出现,甚至出现不代表当前值.
    【解决方案6】:

    在阅读了 pdf 参考指南之后,我发现您可以通过添加值为 1 的“Ff”键(字段标志)来非常轻松地为 AcroForm 字段设置只读模式。 这就是文档的含义:

    如果设置,用户可能不会更改该字段的值。 任何关联的小部件注释都不会交互 与用户;也就是说,他们不会响应鼠标 单击或更改其外观以响应 鼠标动作。此标志对于其字段很有用 值是从数据库计算或导入的。

    所以代码可能看起来像这样(使用 pdfbox lib):

     public static void makeAllWidgetsReadOnly(PDDocument pdDoc) throws IOException {
    
        PDDocumentCatalog catalog = pdDoc.getDocumentCatalog();
    
        PDAcroForm form = catalog.getAcroForm();
    
        List<PDField> acroFormFields = form.getFields();
    
        System.out.println(String.format("found %d acroFrom fields", acroFormFields.size()));
    
        for(PDField field: acroFormFields) {
            makeAcroFieldReadOnly(field);
        }
    }
    
    private static void makeAcroFieldReadOnly(PDField field) {
    
        field.getDictionary().setInt("Ff",1);
    
    }
    

    【讨论】:

    • 这是一个非常有用的信息。感谢您找到旗帜的实际名称。我已经用 PHP 编写了自己的小型 PDF 解析器,并且正在搜索那个标志名称。 :-)
    【解决方案7】:

    我没有足够的评论点,但 SJohnson 将字段设置为只读的回应对我来说非常有效。我在 PDFBox 中使用了类似的东西:

    private void setFieldValueAndFlatten(PDAcroForm form, String fieldName, String fieldValue) throws IOException {
        PDField field = form.getField(fieldName);
        if(field != null){
            field.setValue(fieldValue);
            field.setReadonly(true);
        }
    }
    

    这将写入您的字段值,然后当您在保存后打开 PDF 时,它将具有您的值且不可编辑。

    【讨论】:

      【解决方案8】:

      使用 pdfBox 展平 acroform 并保留表单字段值的解决方案:

      使用 pdfbox 2.0.1 对我有用的解决方案:

      File myFile = new File("myFile.pdf");
      PDDocument pdDoc = PDDocument.load(myFile);
      PDDocumentCatalog pdCatalog = pdDoc.getDocumentCatalog();
      PDAcroForm pdAcroForm = pdCatalog.getAcroForm();
      
      // set the NeedAppearances flag to false
      pdAcroForm.setNeedAppearances(false);
      
      
      field.setValue("new-value");
      
      pdAcroForm.flatten();
      pdDoc.save("myFlattenedFile.pdf");
      pdDoc.close();
      

      我不需要做上述解决方案链接中的 2 个额外步骤:

      // correct the missing page link for the annotations
      // Add the missing resources to the form
      

      我在 OpenOffice 4.1.1 中创建了我的 pdf 表单并导出为 pdf。在 OpenOffice 导出对话框中选择的 2 个项目是:

      1. 已选择“创建 Pdf 表单”
      2. 提交格式为“PDF” - 我发现这提供的 pdf 文件比选择“FDF”更小,但仍以 pdf 形式运行。

      我使用 PdfBox 填充了表单域并创建了一个扁平化的 pdf 文件,该文件删除了表单域但保留了表单域值。

      【讨论】:

      • 这对我有用。它还解决了我在对某些 PDF 表单进行数字签名时遇到的问题(我在链接的解决方案中完成了所有步骤,我猜这些额外的步骤纠正了格式错误的 pdf 内部结构。
      【解决方案9】:

      使用 PDFBox 2,现在可以通过在 PDAcroForm 对象上调用 flatten 方法轻松“展平” PDF 表单。请参阅 Javadoc:PDAcroForm.flatten()

      带有此方法调用示例的简化代码:

      //Load the document
      PDDocument pDDocument = PDDocument.load(new File("E:\\Form-Test.pdf"));    
      PDAcroForm pDAcroForm = pDDocument.getDocumentCatalog().getAcroForm();
      
      //Fill the document
      ...
      
      //Flatten the document
      pDAcroForm.flatten();
      
      //Save the document
      pDDocument.save("E:\\Form-Test-Result.pdf");
      pDDocument.close();
      

      注意:动态 XFA 表单不能展平。

      关于从 PDFBox 1.* 迁移到 2.0,请查看the official migration guide

      【讨论】:

      • 不错 -> pDAcroForm.flatten();使用 org.apache.pdfbox 2.0.4
      • 像魅力一样工作。
      【解决方案10】:

      我想我会分享我们在 PDFBox 2+ 上使用的方法。

      我们使用了PDAcroForm.flatten() 方法。

      字段需要一些预处理,最重要的是必须遍历嵌套字段结构并检查 DV 和 V 的值。

      最后起作用的是:

      private static void flattenPDF(String src, String dst) throws IOException {
          PDDocument doc = PDDocument.load(new File(src));
      
          PDDocumentCatalog catalog = doc.getDocumentCatalog();
          PDAcroForm acroForm = catalog.getAcroForm();
          PDResources resources = new PDResources();
          acroForm.setDefaultResources(resources);
      
          List<PDField> fields = new ArrayList<>(acroForm.getFields());
          processFields(fields, resources);
          acroForm.flatten();
      
          doc.save(dst);
          doc.close();
      }
      
      private static void processFields(List<PDField> fields, PDResources resources) {
          fields.stream().forEach(f -> {
              f.setReadOnly(true);
              COSDictionary cosObject = f.getCOSObject();
              String value = cosObject.getString(COSName.DV) == null ?
                             cosObject.getString(COSName.V) : cosObject.getString(COSName.DV);
              System.out.println("Setting " + f.getFullyQualifiedName() + ": " + value);
              try {
                  f.setValue(value);
              } catch (IOException e) {
                  if (e.getMessage().matches("Could not find font: /.*")) {
                      String fontName = e.getMessage().replaceAll("^[^/]*/", "");
                      System.out.println("Adding fallback font for: " + fontName);
                      resources.put(COSName.getPDFName(fontName), PDType1Font.HELVETICA);
                      try {
                          f.setValue(value);
                      } catch (IOException e1) {
                          e1.printStackTrace();
                      }
                  } else {
                      e.printStackTrace();
                  }
              }
              if (f instanceof PDNonTerminalField) {
                  processFields(((PDNonTerminalField) f).getChildren(), resources);
              }
          });
      }
      

      【讨论】:

        【解决方案11】:

        如果 PDF 文档实际上不包含表单域,但您仍想展平其他元素(如标记),则以下操作非常有效。仅供参考,它是为 C# 实现的

            public static void FlattenPdf(string fileName)
                    {
                        PDDocument doc = PDDocument.load(new java.io.File(fileName));
            
                        java.util.List annots = doc.getPage(0).getAnnotations();
                        for (int i = 0; i < annots.size(); ++i)
                        {
                            PDAnnotation annot = (PDAnnotation)annots.get(i);
                            annot.setLocked(true);
                            annot.setReadOnly(true);
                            annot.setNoRotate(true);
                        }
            
                        doc.save(fileName);
                        doc.close();
            }
        

        这有效地锁定了文档中的所有标记,它们将不再可编辑。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-06-30
          • 2015-01-19
          • 2016-01-27
          • 1970-01-01
          • 1970-01-01
          • 2011-10-15
          • 1970-01-01
          相关资源
          最近更新 更多