| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.pdfbox.pdmodel.interactive.form; |
| |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Rectangle2D; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.pdfbox.cos.COSArray; |
| import org.apache.pdfbox.cos.COSBase; |
| import org.apache.pdfbox.cos.COSDictionary; |
| import org.apache.pdfbox.cos.COSName; |
| import org.apache.pdfbox.pdmodel.PDDocument; |
| import org.apache.pdfbox.pdmodel.PDPage; |
| import org.apache.pdfbox.pdmodel.PDPageContentStream; |
| import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; |
| import org.apache.pdfbox.pdmodel.PDPageTree; |
| import org.apache.pdfbox.pdmodel.PDResources; |
| import org.apache.pdfbox.pdmodel.common.COSArrayList; |
| import org.apache.pdfbox.pdmodel.common.COSObjectable; |
| import org.apache.pdfbox.pdmodel.common.PDRectangle; |
| import org.apache.pdfbox.pdmodel.fdf.FDFCatalog; |
| import org.apache.pdfbox.pdmodel.fdf.FDFDictionary; |
| import org.apache.pdfbox.pdmodel.fdf.FDFDocument; |
| import org.apache.pdfbox.pdmodel.fdf.FDFField; |
| import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; |
| import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; |
| import org.apache.pdfbox.util.Matrix; |
| |
| /** |
| * An interactive form, also known as an AcroForm. |
| * |
| * @author Ben Litchfield |
| */ |
| public final class PDAcroForm implements COSObjectable |
| { |
| private static final Log LOG = LogFactory.getLog(PDAcroForm.class); |
| |
| private static final int FLAG_SIGNATURES_EXIST = 1; |
| private static final int FLAG_APPEND_ONLY = 1 << 1; |
| |
| private final PDDocument document; |
| private final COSDictionary dictionary; |
| |
| private Map<String, PDField> fieldCache; |
| |
| private ScriptingHandler scriptingHandler; |
| |
| /** |
| * Constructor. |
| * |
| * @param doc The document that this form is part of. |
| */ |
| public PDAcroForm(PDDocument doc) |
| { |
| document = doc; |
| dictionary = new COSDictionary(); |
| dictionary.setItem(COSName.FIELDS, new COSArray()); |
| } |
| |
| /** |
| * Constructor. |
| * |
| * @param doc The document that this form is part of. |
| * @param form The existing acroForm. |
| */ |
| public PDAcroForm(PDDocument doc, COSDictionary form) |
| { |
| document = doc; |
| dictionary = form; |
| } |
| |
| /** |
| * This will get the document associated with this form. |
| * |
| * @return The PDF document. |
| */ |
| PDDocument getDocument() |
| { |
| return document; |
| } |
| |
| @Override |
| public COSDictionary getCOSObject() |
| { |
| return dictionary; |
| } |
| |
| /** |
| * This method will import an entire FDF document into the PDF document |
| * that this acroform is part of. |
| * |
| * @param fdf The FDF document to import. |
| * |
| * @throws IOException If there is an error doing the import. |
| */ |
| public void importFDF(FDFDocument fdf) throws IOException |
| { |
| List<FDFField> fields = fdf.getCatalog().getFDF().getFields(); |
| if (fields != null) |
| { |
| for (FDFField field : fields) |
| { |
| FDFField fdfField = field; |
| PDField docField = getField(fdfField.getPartialFieldName()); |
| if (docField != null) |
| { |
| docField.importFDF(fdfField); |
| } |
| } |
| } |
| } |
| |
| /** |
| * This will export all FDF form data. |
| * |
| * @return An FDF document used to export the document. |
| * @throws IOException If there is an error when exporting the document. |
| */ |
| public FDFDocument exportFDF() throws IOException |
| { |
| FDFDocument fdf = new FDFDocument(); |
| FDFCatalog catalog = fdf.getCatalog(); |
| FDFDictionary fdfDict = new FDFDictionary(); |
| catalog.setFDF(fdfDict); |
| |
| List<PDField> fields = getFields(); |
| List<FDFField> fdfFields = new ArrayList<>(fields.size()); |
| for (PDField field : fields) |
| { |
| fdfFields.add(field.exportFDF()); |
| } |
| |
| fdfDict.setID(document.getDocument().getDocumentID()); |
| |
| if (!fdfFields.isEmpty()) |
| { |
| fdfDict.setFields(fdfFields); |
| } |
| return fdf; |
| } |
| |
| /** |
| * This will flatten all form fields. |
| * |
| * <p>Flattening a form field will take the current appearance and make that part |
| * of the pages content stream. All form fields and annotations associated are removed.</p> |
| * |
| * <p>Invisible and hidden fields will be skipped and will not become part of the |
| * page content stream</p> |
| * |
| * <p>The appearances for the form fields widgets will <strong>not</strong> be generated<p> |
| * |
| * @throws IOException |
| */ |
| public void flatten() throws IOException |
| { |
| // for dynamic XFA forms there is no flatten as this would mean to do a rendering |
| // from the XFA content into a static PDF. |
| if (xfaIsDynamic()) |
| { |
| LOG.warn("Flatten for a dynamix XFA form is not supported"); |
| return; |
| } |
| |
| List<PDField> fields = new ArrayList<>(); |
| for (PDField field: getFieldTree()) |
| { |
| fields.add(field); |
| } |
| flatten(fields, false); |
| } |
| |
| |
| /** |
| * This will flatten the specified form fields. |
| * |
| * <p>Flattening a form field will take the current appearance and make that part |
| * of the pages content stream. All form fields and annotations associated are removed.</p> |
| * |
| * <p>Invisible and hidden fields will be skipped and will not become part of the |
| * page content stream</p> |
| * |
| * @param fields |
| * @param refreshAppearances if set to true the appearances for the form field widgets will be updated |
| * @throws IOException |
| */ |
| public void flatten(List<PDField> fields, boolean refreshAppearances) throws IOException |
| { |
| // Nothing to flatten if there are no fields provided |
| if (fields.isEmpty()) |
| { |
| return; |
| } |
| |
| if (!refreshAppearances && getNeedAppearances()) |
| { |
| LOG.warn("acroForm.getNeedAppearances() returns true, " + |
| "visual field appearances may not have been set"); |
| LOG.warn("call acroForm.refreshAppearances() or " + |
| "use the flatten() method with refreshAppearances parameter"); |
| } |
| |
| // for dynamic XFA forms there is no flatten as this would mean to do a rendering |
| // from the XFA content into a static PDF. |
| if (xfaIsDynamic()) |
| { |
| LOG.warn("Flatten for a dynamix XFA form is not supported"); |
| return; |
| } |
| |
| // refresh the appearances if set |
| if (refreshAppearances) |
| { |
| refreshAppearances(fields); |
| } |
| |
| PDPageTree pages = document.getPages(); |
| Map<COSDictionary,Set<COSDictionary>> pagesWidgetsMap = buildPagesWidgetsMap(fields, pages); |
| |
| // preserve all non widget annotations |
| for (PDPage page : pages) |
| { |
| Set<COSDictionary> widgetsForPageMap = pagesWidgetsMap.get(page.getCOSObject()); |
| |
| // indicates if the original content stream |
| // has been wrapped in a q...Q pair. |
| boolean isContentStreamWrapped = false; |
| |
| List<PDAnnotation> annotations = new ArrayList<>(); |
| |
| for (PDAnnotation annotation: page.getAnnotations()) |
| { |
| if (widgetsForPageMap == null || !widgetsForPageMap.contains(annotation.getCOSObject())) |
| { |
| annotations.add(annotation); |
| } |
| else if (isVisibleAnnotation(annotation)) |
| { |
| try (PDPageContentStream contentStream = new PDPageContentStream( |
| document, page, AppendMode.APPEND, true, !isContentStreamWrapped)) |
| { |
| isContentStreamWrapped = true; |
| |
| PDAppearanceStream appearanceStream = annotation.getNormalAppearanceStream(); |
| |
| PDFormXObject fieldObject = new PDFormXObject(appearanceStream.getCOSObject()); |
| |
| contentStream.saveGraphicsState(); |
| |
| // see https://stackoverflow.com/a/54091766/1729265 for an explanation |
| // of the steps required |
| // this will transform the appearance stream form object into the rectangle of the |
| // annotation bbox and map the coordinate systems |
| Matrix transformationMatrix = resolveTransformationMatrix(annotation, appearanceStream); |
| |
| contentStream.transform(transformationMatrix); |
| contentStream.drawForm(fieldObject); |
| contentStream.restoreGraphicsState(); |
| } |
| } |
| } |
| page.setAnnotations(annotations); |
| } |
| |
| // remove the fields |
| removeFields(fields); |
| |
| // remove XFA for hybrid forms |
| dictionary.removeItem(COSName.XFA); |
| |
| // remove SigFlags if no signature left |
| if (this.document.getSignatureDictionaries().isEmpty()) |
| { |
| this.getCOSObject().removeItem(COSName.SIG_FLAGS); |
| } |
| } |
| |
| private boolean isVisibleAnnotation(PDAnnotation annotation) |
| { |
| if (annotation.isInvisible() || annotation.isHidden()) |
| { |
| return false; |
| } |
| PDAppearanceStream normalAppearanceStream = annotation.getNormalAppearanceStream(); |
| if (normalAppearanceStream == null) |
| { |
| return false; |
| } |
| PDRectangle bbox = normalAppearanceStream.getBBox(); |
| return bbox != null && bbox.getWidth() > 0 && bbox.getHeight() > 0; |
| } |
| |
| /** |
| * Refreshes the appearance streams and appearance dictionaries for |
| * the widget annotations of all fields. |
| * |
| * @throws IOException |
| */ |
| public void refreshAppearances() throws IOException |
| { |
| for (PDField field : getFieldTree()) |
| { |
| if (field instanceof PDTerminalField) |
| { |
| ((PDTerminalField) field).constructAppearances(); |
| } |
| } |
| } |
| |
| /** |
| * Refreshes the appearance streams and appearance dictionaries for |
| * the widget annotations of the specified fields. |
| * |
| * @param fields |
| * @throws IOException |
| */ |
| public void refreshAppearances(List<PDField> fields) throws IOException |
| { |
| for (PDField field : fields) |
| { |
| if (field instanceof PDTerminalField) |
| { |
| ((PDTerminalField) field).constructAppearances(); |
| } |
| } |
| } |
| |
| |
| /** |
| * This will return all of the documents root fields. |
| * |
| * A field might have children that are fields (non-terminal field) or does not |
| * have children which are fields (terminal fields). |
| * |
| * The fields within an AcroForm are organized in a tree structure. The documents root fields |
| * might either be terminal fields, non-terminal fields or a mixture of both. Non-terminal fields |
| * mark branches which contents can be retrieved using {@link PDNonTerminalField#getChildren()}. |
| * |
| * @return A list of the documents root fields, never null. If there are no fields then this |
| * method returns an empty list. |
| */ |
| public List<PDField> getFields() |
| { |
| COSArray cosFields = dictionary.getCOSArray(COSName.FIELDS); |
| if (cosFields == null) |
| { |
| return Collections.emptyList(); |
| } |
| List<PDField> pdFields = new ArrayList<>(); |
| for (int i = 0; i < cosFields.size(); i++) |
| { |
| COSDictionary element = (COSDictionary) cosFields.getObject(i); |
| if (element != null) |
| { |
| PDField field = PDField.fromDictionary(this, element, null); |
| if (field != null) |
| { |
| pdFields.add(field); |
| } |
| } |
| } |
| return new COSArrayList<>(pdFields, cosFields); |
| } |
| |
| /** |
| * Set the documents root fields. |
| * |
| * @param fields The fields that are part of the documents root fields. |
| */ |
| public void setFields(List<PDField> fields) |
| { |
| dictionary.setItem(COSName.FIELDS, new COSArray(fields)); |
| } |
| |
| /** |
| * Returns an iterator which walks all fields in the field tree, in order. |
| */ |
| public Iterator<PDField> getFieldIterator() |
| { |
| return new PDFieldTree(this).iterator(); |
| } |
| |
| /** |
| * Return the field tree representing all form fields |
| */ |
| public PDFieldTree getFieldTree() |
| { |
| return new PDFieldTree(this); |
| } |
| |
| /** |
| * This will tell this form to cache the fields into a Map structure |
| * for fast access via the getField method. The default is false. You would |
| * want this to be false if you were changing the COSDictionary behind the scenes, |
| * otherwise setting this to true is acceptable. |
| * |
| * @param cache A boolean telling if we should cache the fields. |
| */ |
| public void setCacheFields(boolean cache) |
| { |
| if (cache) |
| { |
| fieldCache = new HashMap<>(); |
| |
| for (PDField field : getFieldTree()) |
| { |
| fieldCache.put(field.getFullyQualifiedName(), field); |
| } |
| } |
| else |
| { |
| fieldCache = null; |
| } |
| } |
| |
| /** |
| * This will tell if this acro form is caching the fields. |
| * |
| * @return true if the fields are being cached. |
| */ |
| public boolean isCachingFields() |
| { |
| return fieldCache != null; |
| } |
| |
| /** |
| * This will get a field by name, possibly using the cache if setCache is true. |
| * |
| * @param fullyQualifiedName The name of the field to get. |
| * @return The field with that name of null if one was not found. |
| */ |
| public PDField getField(String fullyQualifiedName) |
| { |
| // get the field from the cache if there is one. |
| if (fieldCache != null) |
| { |
| return fieldCache.get(fullyQualifiedName); |
| } |
| |
| // get the field from the field tree |
| for (PDField field : getFieldTree()) |
| { |
| if (field.getFullyQualifiedName().equals(fullyQualifiedName)) |
| { |
| return field; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Get the default appearance. |
| * |
| * @return the DA element of the dictionary object |
| */ |
| public String getDefaultAppearance() |
| { |
| return dictionary.getString(COSName.DA,""); |
| } |
| |
| /** |
| * Set the default appearance. |
| * |
| * @param daValue a string describing the default appearance |
| */ |
| public void setDefaultAppearance(String daValue) |
| { |
| dictionary.setString(COSName.DA, daValue); |
| } |
| |
| /** |
| * True if the viewing application should construct the appearances of all field widgets. |
| * The default value is false. |
| * |
| * @return the value of NeedAppearances, false if the value isn't set |
| */ |
| public boolean getNeedAppearances() |
| { |
| return dictionary.getBoolean(COSName.NEED_APPEARANCES, false); |
| } |
| |
| /** |
| * Set the NeedAppearances value. If this is false, PDFBox will create appearances for all field |
| * widget. |
| * |
| * @param value the value for NeedAppearances |
| */ |
| public void setNeedAppearances(Boolean value) |
| { |
| dictionary.setBoolean(COSName.NEED_APPEARANCES, value); |
| } |
| |
| /** |
| * This will get the default resources for the AcroForm. |
| * |
| * @return The default resources or null if there is none. |
| */ |
| public PDResources getDefaultResources() |
| { |
| COSDictionary dr = dictionary.getCOSDictionary(COSName.DR); |
| return dr != null ? new PDResources(dr, document.getResourceCache()) : null; |
| } |
| |
| /** |
| * This will set the default resources for the acroform. |
| * |
| * @param dr The new default resources. |
| */ |
| public void setDefaultResources(PDResources dr) |
| { |
| dictionary.setItem(COSName.DR, dr); |
| } |
| |
| /** |
| * This will tell if the AcroForm has XFA content. |
| * |
| * @return true if the AcroForm is an XFA form |
| */ |
| public boolean hasXFA() |
| { |
| return dictionary.containsKey(COSName.XFA); |
| } |
| |
| /** |
| * This will tell if the AcroForm is a dynamic XFA form. |
| * |
| * @return true if the AcroForm is a dynamic XFA form |
| */ |
| public boolean xfaIsDynamic() |
| { |
| return hasXFA() && getFields().isEmpty(); |
| } |
| |
| /** |
| * Get the XFA resource, the XFA resource is only used for PDF 1.5+ forms. |
| * |
| * @return The xfa resource or null if it does not exist. |
| */ |
| public PDXFAResource getXFA() |
| { |
| COSBase base = dictionary.getDictionaryObject(COSName.XFA); |
| return base != null ? new PDXFAResource(base) : null; |
| } |
| |
| /** |
| * Set the XFA resource, this is only used for PDF 1.5+ forms. |
| * |
| * @param xfa The xfa resource. |
| */ |
| public void setXFA(PDXFAResource xfa) |
| { |
| dictionary.setItem(COSName.XFA, xfa); |
| } |
| |
| /** |
| * This will get the document-wide default value for the quadding/justification of variable text |
| * fields. |
| * <p> |
| * 0 - Left(default)<br> |
| * 1 - Centered<br> |
| * 2 - Right<br> |
| * See the QUADDING constants of {@link PDVariableText}. |
| * |
| * @return The justification of the variable text fields. |
| */ |
| public int getQ() |
| { |
| return dictionary.getInt(COSName.Q, 0); |
| } |
| |
| /** |
| * This will set the document-wide default value for the quadding/justification of variable text |
| * fields. See the QUADDING constants of {@link PDVariableText}. |
| * |
| * @param q The justification of the variable text fields. |
| */ |
| public void setQ(int q) |
| { |
| dictionary.setInt(COSName.Q, q); |
| } |
| |
| /** |
| * Determines if SignaturesExist is set. |
| * |
| * @return true if the document contains at least one signature. |
| */ |
| public boolean isSignaturesExist() |
| { |
| return dictionary.getFlag(COSName.SIG_FLAGS, FLAG_SIGNATURES_EXIST); |
| } |
| |
| /** |
| * Set the SignaturesExist bit. |
| * |
| * @param signaturesExist The value for SignaturesExist. |
| */ |
| public void setSignaturesExist(boolean signaturesExist) |
| { |
| dictionary.setFlag(COSName.SIG_FLAGS, FLAG_SIGNATURES_EXIST, signaturesExist); |
| } |
| |
| /** |
| * Determines if AppendOnly is set. |
| * |
| * @return true if the document contains signatures that may be invalidated if the file is saved. |
| */ |
| public boolean isAppendOnly() |
| { |
| return dictionary.getFlag(COSName.SIG_FLAGS, FLAG_APPEND_ONLY); |
| } |
| |
| /** |
| * Set a handler to support JavaScript actions in the form. |
| * |
| * @return scriptingHandler |
| */ |
| public ScriptingHandler getScriptingHandler() |
| { |
| return scriptingHandler; |
| } |
| |
| /** |
| * Set a handler to support JavaScript actions in the form. |
| * |
| * @param scriptingHandler |
| */ |
| public void setScriptingHandler(ScriptingHandler scriptingHandler) |
| { |
| this.scriptingHandler = scriptingHandler; |
| } |
| |
| /** |
| * Set the AppendOnly bit. |
| * |
| * @param appendOnly The value for AppendOnly. |
| */ |
| public void setAppendOnly(boolean appendOnly) |
| { |
| dictionary.setFlag(COSName.SIG_FLAGS, FLAG_APPEND_ONLY, appendOnly); |
| } |
| |
| private Matrix resolveTransformationMatrix(PDAnnotation annotation, PDAppearanceStream appearanceStream) |
| { |
| // 1st step transform appearance stream bbox with appearance stream matrix |
| Rectangle2D transformedAppearanceBox = getTransformedAppearanceBBox(appearanceStream); |
| PDRectangle annotationRect = annotation.getRectangle(); |
| |
| // 2nd step caclulate matrix to transform calculated rectangle into the annotation Rect boundaries |
| Matrix transformationMatrix = new Matrix(); |
| transformationMatrix.translate((float) (annotationRect.getLowerLeftX()-transformedAppearanceBox.getX()), (float) (annotationRect.getLowerLeftY()-transformedAppearanceBox.getY())); |
| transformationMatrix.scale((float) (annotationRect.getWidth()/transformedAppearanceBox.getWidth()), (float) (annotationRect.getHeight()/transformedAppearanceBox.getHeight())); |
| return transformationMatrix; |
| } |
| |
| /** |
| * Calculate the transformed appearance box. |
| * |
| * Apply the Matrix (or an identity transform) to the BBox of |
| * the appearance stream |
| * |
| * @param appearanceStream |
| * @return the transformed rectangle |
| */ |
| private Rectangle2D getTransformedAppearanceBBox(PDAppearanceStream appearanceStream) |
| { |
| Matrix appearanceStreamMatrix = appearanceStream.getMatrix(); |
| PDRectangle appearanceStreamBBox = appearanceStream.getBBox(); |
| GeneralPath transformedAppearanceBox = appearanceStreamBBox.transform(appearanceStreamMatrix); |
| return transformedAppearanceBox.getBounds2D(); |
| } |
| |
| private Map<COSDictionary,Set<COSDictionary>> buildPagesWidgetsMap( |
| List<PDField> fields, PDPageTree pages) throws IOException |
| { |
| Map<COSDictionary,Set<COSDictionary>> pagesAnnotationsMap = new HashMap<>(); |
| boolean hasMissingPageRef = false; |
| |
| for (PDField field : fields) |
| { |
| List<PDAnnotationWidget> widgets = field.getWidgets(); |
| for (PDAnnotationWidget widget : widgets) |
| { |
| PDPage page = widget.getPage(); |
| if (page != null) |
| { |
| fillPagesAnnotationMap(pagesAnnotationsMap, page, widget); |
| } |
| else |
| { |
| hasMissingPageRef = true; |
| } |
| } |
| } |
| |
| if (!hasMissingPageRef) |
| { |
| return pagesAnnotationsMap; |
| } |
| |
| // If there is a widget with a missing page reference we need to build the map reverse i.e. |
| // from the annotations to the widget. |
| LOG.warn("There has been a widget with a missing page reference, will check all page annotations"); |
| for (PDPage page : pages) |
| { |
| for (PDAnnotation annotation : page.getAnnotations()) |
| { |
| if (annotation instanceof PDAnnotationWidget) |
| { |
| fillPagesAnnotationMap(pagesAnnotationsMap, page, (PDAnnotationWidget) annotation); |
| } |
| } |
| } |
| |
| return pagesAnnotationsMap; |
| } |
| |
| private void fillPagesAnnotationMap(Map<COSDictionary, Set<COSDictionary>> pagesAnnotationsMap, |
| PDPage page, PDAnnotationWidget widget) |
| { |
| Set<COSDictionary> widgetsForPage = pagesAnnotationsMap.get(page.getCOSObject()); |
| if (widgetsForPage == null) |
| { |
| widgetsForPage = new HashSet<>(); |
| widgetsForPage.add(widget.getCOSObject()); |
| pagesAnnotationsMap.put(page.getCOSObject(), widgetsForPage); |
| } |
| else |
| { |
| widgetsForPage.add(widget.getCOSObject()); |
| } |
| } |
| |
| private void removeFields(List<PDField> fields) |
| { |
| for (PDField field : fields) |
| { |
| COSArray array; |
| if (field.getParent() == null) |
| { |
| // if the field has no parent, assume it is at root level list, remove it from there |
| array = dictionary.getCOSArray(COSName.FIELDS); |
| } |
| else |
| { |
| // if the field has a parent, then remove from the list there |
| array = field.getParent().getCOSObject().getCOSArray(COSName.KIDS); |
| } |
| array.removeObject(field.getCOSObject()); |
| } |
| } |
| } |