Combining XFA with PDFBox - java

I would like to fill a PDF form with the PDFBox java library.
The PDF form is created with Adobe Live Designer, so it uses the XFA format.
I try to find resources about filling XFA PDF forms with PDFBox, but i haven't any luck so far. I saw that a PDAcroForm.setXFA method is available in the API, but i don't see how to use it.
Do you know if it is possible to fill a PDF Form with PDFBox ?
If yes, is there anywhere a code sample or a tutorial to achieve this ?
If no, what are the best alternatives to achieve this ?

This it the best I was able to manage in the time I was allocated on the problem. I get the pdf saved (in Life Cycle) as optimized (I'm not the one doing the pdf). This is the PDF openning part, XML duplication and then saving:
PDDocument document = PDDocument.load(fileInputStream);
fileInputStream.close();
document.setAllSecurityToBeRemoved(true);
Map<String, String> values = new HashMap<String, String>();
values.put("variable_name", "value");
setFields(document, values); // see code below
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
Document documentXML = form.getXFA().getDocument();
NodeList dataElements = documentXML.getElementsByTagName("xfa:data");
if (dataElements != null) {
for (int i = 0; i < dataElements.getLength(); i++) {
setXFAFields(dataElements.item(i), values);
}
}
COSStream cosout = new COSStream(new RandomAccessBuffer());
TransformerFactory.newInstance().newTransformer()
.transform(new DOMSource(documentXML), new StreamResult(cosout.createUnfilteredStream()));
form.setXFA(new PDXFA(cosout));
FileOutputStream fios = new FileOutputStream(new File(docOut + ".pdf"));
document.save(fios);
document.close();
try {
fios.flush();
} finally {
fios.close();
}
then the methods who set values for fields. I set both the XFA and the AcroForm:
public void setXFAFields(Node pNode, Map<String, String> values) throws IOException {
if (values.containsKey(pNode.getNodeName())) {
pNode.setTextContent(values.get(pNode.getNodeName()));
} else {
NodeList childNodes = pNode.getChildNodes();
if (childNodes != null) {
for (int i = 0; i < childNodes.getLength(); i++) {
setXFAFields(childNodes.item(i), values);
}
}
}
}
public void setFields(PDDocument pdfDocument, Map<String, String> values) throws IOException {
#SuppressWarnings("unchecked")
List<PDField> fields = pdfDocument.getDocumentCatalog().getAcroForm().getFields();
for (PDField pdField : fields) {
setFields(pdField, values);
}
}
private void setFields(PDField field, Map<String, String> values) throws IOException {
List<COSObjectable> kids = field.getKids();
if (kids != null) {
for (COSObjectable pdfObj : kids) {
if (pdfObj instanceof PDField) {
setFields((PDField) pdfObj, values);
}
}
} else {
// remove the [0] from the name to match values in our map
String partialName = field.getPartialName().replaceAll("\\[\\d\\]", "");
if (!(field instanceof PDSignatureField) && values.containsKey(partialName)) {
field.setValue(values.get(partialName));
}
}
}
This work, but not for all "kind" of PDF life Cycle produce, some got a warning message about "extended fonction" not enabled anymore but still work. The optimize version is the only one I found who don't prompt message when openned after being filled.
I fill the XFA and the Acroform otherwise it don't work in all viewer.

The question specifically identifies the PDFBox library in the subject; you do not need iText, the XFA manipulation can be done using the PDXFA object available in PDFBox 1.8.
Many thanks to Maruan Sahyoun for his great work on PDFBox + XFA.
This code only works when you remove all security on the PDDocument.
It also assumes the COS object in PDXFA is a COSStream.
The simplistic example below reads the xml stream and writes it back into the PDF.
PDDocument doc = PDDocument.load("filename");
doc.setAllSecurityToBeRemoved(true);
PDDocumentCatalog docCatalog = doc.getDocumentCatalog();
PDAcroForm form = docCatalog.getAcroForm();
PDXFA xfa = form.getXFA();
COSBase cos = xfa.getCOSObject();
COSStream coss = (COSStream) cos;
InputStream cosin = coss.getUnfilteredStream();
Document document = documentBuilder.parse(cosin);
COSStream cosout = new COSStream(new RandomAccessBuffer());
OutputStream out = cosout.createUnfilteredStream();
TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer();
DOMSource source = new DOMSource(xmlDoc);
StreamResult result = new StreamResult(out);
transformer.transform(source, result);
PDXFA xfaout = new PDXFA(cosout);
form.setXFA(xfaout);

I'm not familiar with pdfbox but you can do this with iText (http://itextpdf.com/) once you get access to the XFA (XML) DOM.

Try this and it will merge all pdf with no xfa and with XFA(this is when using PDBox only)
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
if(form != null) {
document.setAllSecurityToBeRemoved(true);
form.flatten();
if(form.hasXFA()) {
form.setXFA(null);
}
}
merge.appendDocument(anyPDFDoc, document);

AcroForm is for PDF with static fields. If PDF have xfa forms you can use itext (Java) or itextsharp ( .net) to Populate your Data . Only problem with XFA Forms are they cannot be Flatten with itext only way to Flatten I found is using bullzip or similar pdf creator to open that xfa pdf created with itext and pass it through bullzip which will spit out flatten pdf version. Hope this will give u some ideas.
Below code just a rough idea how xfa is filled.
XfaForm xfa = pdfFormFields.Xfa;
dynamic bytes = Encoding.UTF8.GetBytes("<?xml version=\"1.0\" encoding=\"UTF-8\"?> <form1> <staticform>" + "\r\n<barcode>" + barcode + "</barcode></staticform> <flowForm><Extra>" + Extra + "</Extra></flowForm> </form1>");
MemoryStream ms = new MemoryStream(bytes);
pdfStamper.AcroFields.Xfa.FillXfaForm(ms);
you can now use the xfa pdf you created and print through bullzip
const string Printer_Name = "Bullzip PDF Printer";
PdfSettings pdfSettings = new PdfSettings();
pdfSettings.PrinterName = Printer_Name;
pdfSettings.SetValue("Output", flatten_pdf);
pdfSettings.SetValue("ShowPDF", "no");
pdfSettings.SetValue("ShowSettings", "never");
pdfSettings.SetValue("ShowSaveAS", "never");
pdfSettings.SetValue("ShowProgress", "no");
pdfSettings.SetValue("ShowProgressFinished", "no");
pdfSettings.SetValue("ConfirmOverwrite", "no");
pdfSettings.WriteSettings(PdfSettingsFileType.RunOnce);
PdfUtil.PrintFile(xfa_pdffile, Printer_Name);
output file will be flatten pdf..

Related

apache pdfbox - how to test if a document is flattened?

I have written the following small Java main method. It takes in a (hardcoded for testing purposes!) PDF document I know contains active elements in the form and need to flatten it.
public static void main(String [] args) {
try {
// for testing
Tika tika = new Tika();
String filePath = "<path-to>/<pdf-document-with-active-elements>.pdf";
String fileName = filePath.substring(0, filePath.length() -4);
File file = new File(filePath);
if (tika.detect(file).equalsIgnoreCase("application/pdf")) {
PDDocument pdDocument = PDDocument.load(file);
PDAcroForm pdAcroForm = pdDocument.getDocumentCatalog().getAcroForm();
if (pdAcroForm != null) {
pdAcroForm.flatten();
pdAcroForm.refreshAppearances();
pdDocument.save(fileName + "-flattened.pdf");
}
pdDocument.close();
}
}
catch (Exception e) {
System.err.println("Exception: " + e.getLocalizedMessage());
}
}
What kind of test would assert the File(<path-to>/<pdf-document-with-active-elements>-flattened.pdf) generated by this code would, in fact, be flat?
What kind of test would assert that the file generated by this code would, in fact, be flat?
Load that document anew and check whether it has any form fields in its PDAcroForm (if there is a PDAcroForm at all).
If you want to be thorough, also iterate through the pages and assure that there are no Widget annotations associated to them anymore.
And to really be thorough, additionally determine the field positions and contents before flattening and apply text extraction at those positions to the flattened pdf. This verifies that the form has not merely been dropped but indeed flattened.

How to extract XML from XFA PDF document in Java using iText 7 (or other)?

Using Java and iText 7, I am trying to exact the XML data from a XFA PDF form in order to parse (and possibly modify) the data but all I can manage to do is grab some basic generic data that is the same for any XFA file I use.
I know it has to be possible since it is done in the iText RUPS tool but I have been going in circles for days now.
public class Parse {
private PdfDocument pdf;
private PdfAcroForm form;
private XfaForm xfa;
private Document domDocument;
private Map<Integer, String> data;
private int numberOfPages;
private String pdfText;
public void openPdf(String src, String dest) throws IOException, TransformerException {
PdfReader reader = new PdfReader(src);
reader.setUnethicalReading(true);
pdf = new PdfDocument(reader, new PdfWriter(dest));
form = PdfAcroForm.getAcroForm(pdf, true);
data = new HashMap<Integer, String>();
numberOfPages = getNumberOfPdfPages();
PdfPage currentPage;
String textFromPage;
for (int page = 1; page <= numberOfPages; page++) {
System.out.println("Reading page: " + page + " -----------------");
currentPage = pdf.getPage(page);
textFromPage = PdfTextExtractor.getTextFromPage(currentPage);
data.put(page, textFromPage);
pdfText += currentPage + ":" + "\n" + textFromPage + "\n";
}
xfa = form.getXfaForm();
domDocument = xfa.getDomDocument();
Map<String, Node> map = xfa.extractXFANodes(domDocument);
System.out.println("The template node = " + map.get("template").toString() + "\n");
System.out.println("Dom document = " + domDocument.toString() + "\n");
System.out.println("In map form = " + map.toString() + "\n");
System.out.println("pdfText = " + pdfText + "\n");
Node node = xfa.getDatasetsNode();
NodeList list = node.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
System.out.println("Get Child Nodes Output = " + list.item(i) + "\n");
}
}
}
This is the generic output I am receiving.
Reading page: 1 -----------------
The template node = [template: null]
Dom document = [#document: null]
In map form = {template=[template: null], form=[form: null], xfdf=[xfdf: null], xmpmeta=[x:xmpmeta: null], datasets=[xfa:datasets: null], config=[config: null], PDFSecurity=[PDFSecurity: null]}
pdfText = nullcom.itextpdf.kernel.pdf.PdfPage#6fa38a:
> Please wait...
>
> If this message is not eventually replaced by the proper contents of
> the document, your PDF viewer may not be able to display this type of
> document. You can upgrade to the latest version of Adobe Reader
> for Windows®, Mac, or Linux® by visiting
> http://www.adobe.com/go/reader_download. For more assistance with
> Adobe Reader visit http://www.adobe.com/go/acrreader. Windows is
> either a registered trademark or a trademark of Microsoft Corporation
> in the United States and/or other countries. Mac is a trademark of
> Apple Inc., registered in the United States and other countries. Linux
> is the registered trademark of Linus Torvalds in the U.S. and other
> countries.
Get Child Nodes Output = [xfa:data: null]
You have a file that is a pure XFA file. This means that the only PDF content that is stored in this file consists of the "Please wait..." message. That page is shown in PDF viewer that don't know how to render XFA.
It's also the content you get when you extract the content from the page using:
currentPage = pdf.getPage(page);
textFromPage = PdfTextExtractor.getTextFromPage(currentPage);
This is something you shouldn't do when facing a pure XFA file, because all the relevant content is stored in the XML stream that is stored inside the PDF file.
You already have the first part right:
xfa = form.getXfaForm();
domDocument = xfa.getDomDocument();
The XFA stream is to be found in the /AcroForm entry. I know this is awkward, but that's how PDF was designed. That's not our choice, and XFA is deprecated in PDF 2.0, so XFA is dying anyway. The problem will disappear when XFA is finally dead and buried.
This being said, you have an instance of a org.w3c.dom.Document and you want to get the XML file stored in this object. You don't need iText to do this. That's explained for instance in Converting a org.w3c.dom.Document in Java to String using Transformer
I tested that code on an XFA file using this snippet:
public static void main(String[] args) throws IOException, TransformerException {
PdfDocument pdf = new PdfDocument(new PdfReader(SRC));
PdfAcroForm form = PdfAcroForm.getAcroForm(pdf, true);
XfaForm xfa = form.getXfaForm();
Document doc = xfa.getDomDocument();
DOMSource domSource = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(domSource, result);
writer.flush();
System.out.println(writer.toString());
}
The output to the screen was the XDP XML file with all the XFA information I expected.
Note that I would be careful when replacing the XFA XML file. It's better not to meddle with the XFA structure, but to create an XML file containing nothing but the data created using the appropriate schema, and to fill the form as described in the FAQ: How to fill out a pdf file programmatically? (Dynamic XFA)

Java : BOLD with iText in PDF Generation doesn't work correctly

I use iText for generating PDF, from a XML file, with content in HTML. Everything is working, except one little thing.
When I have a bloc of text containing a part in BOLD, the BOLD doesn't appear in the resulting PDF file. If I have a complete phrase in BOLD, it's working fine.
Examples :
<DIV><FONT face='Arial' size='10'><B>The BOLD for this phrase works</B></FONT></DIV>
<DIV><FONT face='Arial' size='10'>The BOLD for <B>this part of the phrase </B> doesn't work</FONT></DIV>
With 'Italic' or 'Underline', I can do the same test but I don't have the problem. It's working...
A little precision : if I use a tag <B> combined with a tag <U> or <I>, for a part of bloc of text, it's working too.
Example :
<DIV><FONT face='Arial' size='10'>The combination of <B><I>BOLD and something else (U or I)</I></B> works fine.</FONT></DIV>
For the context : WebApp with struts, the PDF is not saved as a file but sent to the navigator as a response. As suggested by an answer, I update my version of iText from 1.4.8 to 5.5.7.
For the HTML code saved in a xml file, you can see examples above.
For the Java code (I picked up the code from severals long methods. I hope I forgot nothing...).
ByteArrayOutputStream baoutLettre = new ByteArrayOutputStream();
Document document = new Document();
PdfWriter myWriter = PdfWriter.getInstance(document, baoutLettre);
handleHeaderFooter(request, response, document, Constantes.Type_LETTRE);
document.open();
String lettreContent = FileHelper.readFile("myLetter.xml");
XmlParser.parse(document, new ByteArrayInputStream(lettreContent.getBytes("UTF-8")), getTagMap());
document.close();
ByteArrayOutputStream outTmp = new ByteArrayOutputStream(64000);
PdfCopyMerge pdfCM = new PdfCopyMerge(outTmp);
pdfCM.addDocument(baoutLettre.toByteArray());
pdfCM.close();
ByteArrayOutputStream outPDF = addPageNumber(outTmp.toByteArray(), soc, dicoEdition, request);
outPDF.writeTo(request.getOutputStream());
And for the class PdfCopyMerge :
public class PdfCopyMerge {
private ByteArrayOutputStream outStream = new ByteArrayOutputStream();
private Document document = null;
private PdfCopy writer = null;
public PdfCopyMerge(ByteArrayOutputStream stream) {
super();
outStream = stream;
}
public int addDocument(byte[] pdfByteArray) {
int numberOfPages = 0;
try {
PdfReader reader = new PdfReader(pdfByteArray);
numberOfPages = reader.getNumberOfPages();
if (this.document == null) {
this.document = new Document(reader.getPageSizeWithRotation(1));
this.writer = new PdfCopy(this.document, this.getOutputStream());
this.document.open();
}
PdfImportedPage page;
for (int i = 0; i < numberOfPages;) {
++i;
page = this.writer.getImportedPage(reader, i);
this.writer.addPage(page);
}
PRAcroForm form = reader.getAcroForm();
if (form != null) {
this.writer.copyAcroForm(reader);
}
} catch (Exception e) {
logger.error(e.getMessage(),e);
}
return numberOfPages;
}
Does anybody face the same problem ? I look for any helping ideas ...
Thanks.
Try the lastest version 5.5.7. Everything works fine.
https://github.com/itext/itextpdf/tags

Remove rectangles from PDF file

I'd like to have a program that removes all rectangles from a PDF file. One use case for this is to unblacken a given PDF file to see if there is any hidden information behind the rectangles. The rest of the PDF file should be kept as-is.
Which PDF library is suitable to this task? In Java, I would like the code to look like this:
PdfDocument doc = PdfDocument.load(new File("original.pdf"));
PdfDocument unblackened = doc.transform(new CopyingPdfVisitor() {
public void visitRectangle(PdfRect rect) {
if (rect.getFillColor().getBrightness() >= 0.1) {
super.visitRectangle(rect);
}
}
});
unblackened.save(new File("unblackened.pdf"));
The CopyingPdfVisitor would copy a PDF document exactly as-is, and my custom code would leave out all the dark rectangles.
Itext pdf library have ways to modify pdf content.
The *ITEXT CONTENTPARSER Example * may give you any idea. "qname" parameter (qualified name) may be used to detected rectangle element.
http://itextpdf.com/book/chapter.php?id=15
Other option, if you want obtain the text on the document use the PdfReaderContentParser to extract text content
public void parsePdf(String pdf, String txt) throws IOException {
PdfReader reader = new PdfReader(pdf);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
PrintWriter out = new PrintWriter(new FileOutputStream(txt));
TextExtractionStrategy strategy;
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
strategy = parser.processContent(i, new SimpleTextExtractionStrategy());
out.println(strategy.getResultantText());
}
out.flush();
out.close();
reader.close();
}
example at http://itextpdf.com/examples/iia.php?id=277

PDFBox: How to "flatten" a PDF-form?

How do I "flatten" a PDF-form (remove the form-field but keep the text of the field) with PDFBox?
Same question was answered here:
a quick way to do this, is to remove the fields from the acrofrom.
For this you just need to get the document catalog, then the acroform
and then remove all fields from this acroform.
The graphical representation is linked with the annotation and stay in
the document.
So I wrote this code:
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();
}
}
With PDFBox 2 it's now possible to "flatten" a PDF-form easily by calling the flatten method on a PDAcroForm object. See Javadoc: PDAcroForm.flatten().
Simplified code with an example call of this method:
//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();
Note: dynamic XFA forms cannot be flatten.
For migration from PDFBox 1.* to 2.0, take a look at the official migration guide.
This works for sure - I've ran into this problem, debugged all-night, but finally figured out how to do this :)
This is assuming that you have capability to edit the PDF in some way/have some control over the PDF.
First, edit the forms using Acrobat Pro. Make them hidden and read-only.
Then you need to use two libraries: PDFBox and PDFClown.
PDFBox removes the thing that tells Adobe Reader that it's a form; PDFClown removes the actual field. PDFClown must be done first, then PDFBox (in that order. The other way around doesn't work).
Single field example code:
// 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");
Probably some typos here and there, but this should be enough to get the gist :)
After reading about pdf reference guide, I have discovered that you can quite easily set read-only mode for AcroForm fields by adding "Ff" key (Field flags) with value 1.
This is what documentation stands about that:
If set, the user may not change the value of the field.
Any associated widget annotations will not interact
with the user; that is, they will not respond to mouse
clicks or change their appearance in response to
mouse motions. This flag is useful for fields whose
values are computed or imported from a database.
so the code could look like that (using 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);
}
setReadOnly did work for me as shown below -
#SuppressWarnings("unchecked")
List<PDField> fields = acroForm.getFields();
for (PDField field : fields) {
if (field.getFullyQualifiedName().equals("formfield1")) {
field.setReadOnly(true);
}
}
Solution to flattening acroform AND retaining the form field values using pdfBox:
see solution at https://mail-archives.apache.org/mod_mbox/pdfbox-users/201604.mbox/%3C3BC7E352-9447-4458-AAC3-5A9B70B4CCAA#fileaffairs.de%3E
The solution that worked for me with 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();
I didn't need to do the 2 extra steps in the above solution link:
// correct the missing page link for the annotations
// Add the missing resources to the form
I created my pdf form in OpenOffice 4.1.1 and exported to pdf. The 2 items selected in the OpenOffice export dialogue were:
selected "create Pdf Form"
Submit format of "PDF" - I found this gave smaller pdf file size than selecting "FDF" but still operated as a pdf form.
Using PdfBox I populated the form fields and created a flattened pdf file that removed the form fields but retained the form field values.
In order to really "flatten" an acrobat form field there seems to be much more to do than at the first glance.
After examining the PDF standard I managed to achieve real flatening in three steps:
save field value
remove widgets
remove form field
All three steps can be done with pdfbox (I used 1.8.5). Below I will sketch how I did it.
A very helpful tool in order to understand whats going on is the PDF Debugger.
Save the field
This is the most complicated step of the three.
In order to save the field's value you have to save its content to the pdf's content for each of the field's widgets. Easiest way to do so is drawing each widget's appearance to the widget's page.
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 );
}
The appearance is an XObject stream containing all of the widget's content (value, font, size, rotation, etc.). You simply need to place it at the right position on the page which you can extract from the widget's rectangle.
Remove widgets
As noted above each field may have multiple widgets. A widget takes care of how a form field can be edited, triggers, displaying when not editing and such stuff.
In order to remove one you have to remove it from its page's annotations.
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 );
}
Note that the annotations may not contain the exact PDAnnotationWidget since it's a kind of a wrapper. You have to remove the one with matching COSObject.
Remove form field
As final step you remove the form field itself. This is not very different to the other posts above.
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 );
}
Note that I used a custom removeAll method here since the removeCandidates.removeAll() didn't work as expected for me.
Sorry that I cannot provide all the code here but with the above you should be able to write it yourself.
I don't have enough points to comment but SJohnson's response of setting the field to read only worked perfectly for me. I am using something like this with 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);
}
}
This will write your field value and then when you open the PDF after saving it will have your value and not be editable.
This is the code I came up with after synthesizing all of the answers I could find on the subject. This handles flattening text boxes, combos, lists, checkboxes, and radios:
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();
}
Full class here:
https://gist.github.com/jribble/beddf7620536939f88db
This is the answer of Thomas, from the PDFBox-Mailinglist:
You will need to get the Fields over the COSDictionary. Try this
code...
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();
I thought I'd share our approach that worked with PDFBox 2+.
We've used the PDAcroForm.flatten() method.
The fields needed some preprocessing and most importantly the nested field structure had to be traversed and DV and V checked for values.
Finally what worked was the following:
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);
}
});
}
If the PDF document doesn't actually contain form fields but you still want to flatten other elements like markups, the following works quite well. FYI It was implemented for 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();
}
This effectively locks all markups in the document and they will no longer be editable.
pdfbox c# annotations

Categories

Resources