11

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

在这里回答了同样的问题:

一种快速的方法是从 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();
    }
}
4

11 回答 11

24

使用 PDFBox 2,现在可以通过调用对象flatten上的方法轻松地“展平” PDF 表单PDAcroForm。请参阅 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,请查看官方迁移指南

于 2016-06-14T18:24:12.187 回答
8

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

假设您有能力以某种方式编辑 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");

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

于 2013-11-01T08:52:38.510 回答
7

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

   @SuppressWarnings("unchecked")
    List<PDField> fields = acroForm.getFields();
    for (PDField field : fields) {
        if (field.getFullyQualifiedName().equals("formfield1")) {
            field.setReadOnly(true);
        }
    }
于 2013-07-18T15:11:19.277 回答
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);

}
于 2015-01-23T17:37:37.537 回答
4

使用 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”格式 - 我发现这提供了比选择“FDF”更小的 pdf 文件大小,但仍作为 pdf 表单操作。

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

于 2016-05-15T19:04:51.117 回答
2

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

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

所有三个步骤都可以用 pdfbox 完成(我用的是 1.8.5)。下面我将概述我是如何做到的。为了了解发生了什么,一个非常有用的工具是PDF 调试器

保存字段

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

为了保存字段的值,您必须将其内容保存到每个字段小部件的 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() 对我来说没有按预期工作。

抱歉,我无法在此处提供所有代码,但有了以上内容,您应该可以自己编写代码。

于 2014-07-08T13:27:52.550 回答
2

我没有足够的评论点,但 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 时,它将具有您的值并且不可编辑。

于 2015-08-27T17:58:57.077 回答
1

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

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

于 2014-06-06T21:57:31.687 回答
0

这是来自 PDFBox-Mailinglist 的 Thomas 的回答:

您将需要获取 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();
于 2013-01-25T07:13:16.987 回答
0

我想我会分享我们与 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);
        }
    });
}
于 2018-07-10T20:32:13.500 回答
0

如果 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();
    }

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

于 2020-06-23T22:19:06.780 回答