com.itextpdf.forms.PdfAcroForm Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.itextpdf.forms;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.commons.utils.StringSplitUtil;
import com.itextpdf.forms.exceptions.FormsExceptionMessageConstant;
import com.itextpdf.forms.fields.AbstractPdfFormField;
import com.itextpdf.forms.fields.PdfFormAnnotation;
import com.itextpdf.forms.fields.PdfFormAnnotationUtil;
import com.itextpdf.forms.fields.PdfFormCreator;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.forms.fields.PdfFormFieldMergeUtil;
import com.itextpdf.forms.fields.merging.MergeFieldsStrategy;
import com.itextpdf.forms.fields.merging.OnDuplicateFormFieldNameStrategy;
import com.itextpdf.forms.logs.FormsLogMessageConstants;
import com.itextpdf.forms.xfa.XfaForm;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.IsoKey;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfBoolean;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfObjectWrapper;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.VersionConforming;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.tagutils.TagReference;
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents the static form technology AcroForm on a PDF file.
*/
public class PdfAcroForm extends PdfObjectWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(PdfAcroForm.class);
/**
* To be used with {@link #setSignatureFlags}.
*
*
* If set, the document contains at least one signature field. This flag
* allows a conforming reader to enable user interface items (such as menu
* items or pushbuttons) related to signature processing without having to
* scan the entire document for the presence of signature fields.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*/
public static final int SIGNATURE_EXIST = 1;
/**
* To be used with {@link #setSignatureFlags}.
*
*
* If set, the document contains signatures that may be invalidated if the
* file is saved (written) in a way that alters its previous contents, as
* opposed to an incremental update. Merely updating the file by appending
* new information to the end of the previous version is safe. Conforming
* readers may use this flag to inform a user requesting a full save that
* signatures will be invalidated and require explicit confirmation before
* continuing with the operation.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*/
public static final int APPEND_ONLY = 2;
/**
* Keeps track of whether or not appearances must be generated by the form
* fields themselves, or by the PDF viewer application. Default is
* true
.
*/
protected boolean generateAppearance = true;
/**
* A map of field names and their associated {@link PdfFormField form field}
* objects.
*/
protected Map fields = new LinkedHashMap<>();
/**
* The PdfDocument to which the PdfAcroForm belongs.
*/
protected PdfDocument document;
private PdfDictionary defaultResources;
private Set fieldsForFlattening = new LinkedHashSet<>();
private XfaForm xfaForm;
/**
* Creates a PdfAcroForm as a wrapper of a dictionary.
* Also initializes an XFA form if an /XFA
entry is present in
* the dictionary.
*
* @param pdfObject the PdfDictionary to be wrapped
*/
private PdfAcroForm(PdfDictionary pdfObject, PdfDocument pdfDocument) {
super(pdfObject);
document = pdfDocument;
fields = populateFormFieldsMap();
xfaForm = new XfaForm(pdfObject);
}
/**
* Creates a PdfAcroForm from a {@link PdfArray} of fields.
* Also initializes an empty XFA form.
*
* @param fields a {@link PdfArray} of {@link PdfDictionary} objects
*/
private PdfAcroForm(PdfArray fields) {
this(createAcroFormDictionaryByFields(fields), null);
setForbidRelease();
}
/**
* Retrieves AcroForm from the document. If there is no AcroForm in the
* document Catalog and createIfNotExist flag is true then the AcroForm
* dictionary will be created and added to the document.
*
* @param document the document to retrieve the {@link PdfAcroForm} from
* @param createIfNotExist when true
, this method will create a {@link PdfAcroForm} if none exists
* for this document
*
* @return the {@link PdfDocument document}'s AcroForm,
* or a new one provided that createIfNotExist
parameter is true
, otherwise
* null
.
*/
public static PdfAcroForm getAcroForm(PdfDocument document, boolean createIfNotExist) {
return getAcroForm(document, createIfNotExist, new MergeFieldsStrategy());
}
/**
* Retrieves AcroForm from the document. If there is no AcroForm in the
* document Catalog and createIfNotExist flag is true then the AcroForm
* dictionary will be created and added to the document.
*
* @param document the document to retrieve the {@link PdfAcroForm} from
* @param createIfNotExist when true
, this method will create a {@link PdfAcroForm} if none
* exists for
* this document
* @param onDuplicateFieldNameStrategy the strategy to be used when a field with the same name already exists
*
* @return the {@link PdfDocument document}'s AcroForm,
* or a new one provided that createIfNotExist
parameter is true
, otherwise
* null
.
*/
public static PdfAcroForm getAcroForm(PdfDocument document, boolean createIfNotExist,
OnDuplicateFormFieldNameStrategy onDuplicateFieldNameStrategy) {
document.getDiContainer().register(OnDuplicateFormFieldNameStrategy.class, onDuplicateFieldNameStrategy);
PdfDictionary acroFormDictionary = document.getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm);
PdfAcroForm acroForm = null;
if (acroFormDictionary == null) {
if (createIfNotExist) {
acroForm = new PdfAcroForm(new PdfArray());
acroForm.makeIndirect(document);
document.getCatalog().put(PdfName.AcroForm, acroForm.getPdfObject());
document.getCatalog().setModified();
}
} else {
acroForm = new PdfAcroForm(acroFormDictionary, document);
}
if (acroForm != null) {
acroForm.defaultResources = acroForm.getDefaultResources();
if (acroForm.defaultResources == null) {
acroForm.defaultResources = new PdfDictionary();
}
acroForm.document = document;
acroForm.xfaForm = new XfaForm(document);
}
return acroForm;
}
/**
* This method adds the field to the last page in the document.
* If there's no pages, creates a new one.
*
* @param field the {@link PdfFormField} to be added to the form
*/
public void addField(PdfFormField field) {
if (!field.getPdfObject().containsKey(PdfName.T)) {
throw new PdfException(FormsExceptionMessageConstant.FORM_FIELD_MUST_HAVE_A_NAME);
}
PdfPage page;
if (document.getNumberOfPages() == 0) {
document.addNewPage();
}
page = document.getLastPage();
addField(field, page);
}
/**
* This method adds the field to a specific page.
*
* @param field the {@link PdfFormField} to be added to the form
* @param page the {@link PdfPage} on which to add the field
*/
public void addField(PdfFormField field, PdfPage page) {
addField(field, page, true);
}
/**
* This method adds the field to a specific page.
*
* @param field the {@link PdfFormField} to be added to the form
* @param page the {@link PdfPage} on which to add the field
* @param throwExceptionOnError true if the exception is expected to be thrown in case of error.
*/
public void addField(PdfFormField field, PdfPage page, boolean throwExceptionOnError) {
if (!field.getPdfObject().containsKey(PdfName.T)) {
if (throwExceptionOnError) {
throw new PdfException(FormsExceptionMessageConstant.FORM_FIELD_MUST_HAVE_A_NAME);
} else {
LOGGER.warn(FormsLogMessageConstants.FORM_FIELD_MUST_HAVE_A_NAME);
return;
}
}
PdfFormFieldMergeUtil.mergeKidsWithSameNames(field, throwExceptionOnError);
// PdfPageFormCopier expects that we replace existed field by a new one in case they have the same names.
if (needToAddToAcroform(field, throwExceptionOnError)) {
PdfArray fieldsArray = getFields();
fieldsArray.add(field.getPdfObject());
fieldsArray.setModified();
fields.put(field.getFieldName().toUnicodeString(), field);
}
PdfDictionary fieldDict = field.getPdfObject();
processKids(fields.get(field.getFieldName().toUnicodeString()), page);
if (fieldDict.containsKey(PdfName.Subtype) && page != null) {
defineWidgetPageAndAddToIt(page, fieldDict, false);
}
setModified();
}
/**
* This method merges field with its annotation and places it on the given
* page. This method also work if the field has more than one widget
* annotation, but doesn't work with no annotations.
*
* @param field the {@link PdfFormField} to be added to the form
* @param page the {@link PdfPage} on which to add the field
*/
public void addFieldAppearanceToPage(PdfFormField field, PdfPage page) {
PdfDictionary fieldDict = field.getPdfObject();
PdfArray kids = field.getKids();
if (kids == null) {
return;
}
if (kids.size() == 1) {
PdfDictionary kidDict = (PdfDictionary) kids.get(0);
if (PdfFormAnnotationUtil.isPureWidget(kidDict)) {
// kid is pure widget, merge it with parent field
PdfFormAnnotationUtil.mergeWidgetWithParentField(field);
defineWidgetPageAndAddToIt(page, fieldDict, false);
return;
}
}
for (int i = 0; i < kids.size(); ++i) {
PdfDictionary kidDict = (PdfDictionary) kids.get(i);
if (PdfFormAnnotationUtil.isPureWidgetOrMergedField(kidDict)) {
// kid is either a pure widget or a merged field
defineWidgetPageAndAddToIt(page, kidDict, false);
}
}
}
/**
* Gets root fields (i.e. direct children of Acroform dictionary).
*
* @return a map of field names and their associated {@link PdfFormField form field} objects
*/
public Map getRootFormFields() {
if (fields.size() == 0) {
fields = populateFormFieldsMap();
}
//TODO DEVSIX-6504 Fix copyField logic.
return fields;
}
/**
* Gets all {@link PdfFormField form field}s as a {@link Map} including fields kids.
*
* @return a map of field names and their associated {@link PdfFormField form field} objects
*/
public Map getAllFormFields() {
if (fields.size() == 0) {
fields = populateFormFieldsMap();
}
final Map allFields = new LinkedHashMap<>(fields);
for (Entry field : fields.entrySet()) {
final List kids = field.getValue().getAllChildFormFields();
for (PdfFormField kid : kids) {
final PdfString kidFieldName = kid.getFieldName();
if (kidFieldName != null) {
allFields.put(kidFieldName.toUnicodeString(), kid);
}
}
}
return allFields;
}
/**
* Gets all {@link AbstractPdfFormField form field}s as a {@link Set} including fields kids and nameless fields.
*
* @return a set of {@link AbstractPdfFormField form field} objects.
*/
public Set getAllFormFieldsAndAnnotations() {
if (fields.isEmpty()) {
fields = populateFormFieldsMap();
}
Set allFields = new LinkedHashSet<>();
for (Entry field : fields.entrySet()) {
allFields.add(field.getValue());
List kids = field.getValue().getAllChildFields();
allFields.addAll(kids);
}
return allFields;
}
/**
* Gets a collection of {@link PdfFormField form field}s, prepared for flattening using {@link #partialFormFlattening} method.
* If returned collection is empty, all form fields will be flattened on {@link #flattenFields flattenFields} call.
*
* @return a collection of {@link PdfFormField form field}s for flattening
*/
public Collection getFieldsForFlattening() {
return Collections.unmodifiableCollection(fieldsForFlattening);
}
/**
* Gets the {@link PdfDocument} this {@link PdfAcroForm} belongs to.
*
* @return the document of this form
*/
public PdfDocument getPdfDocument() {
return document;
}
/**
* Sets the NeedAppearances
boolean property on the AcroForm.
* NeedAppearances has been deprecated in PDF 2.0.
*
*
* NeedAppearances is a flag specifying whether to construct appearance
* streams and appearance dictionaries for all widget annotations in the
* document.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @param needAppearances a boolean. Default value is false
* @return current AcroForm.
*/
public PdfAcroForm setNeedAppearances(boolean needAppearances) {
if (VersionConforming.validatePdfVersionForDeprecatedFeatureLogError(document, PdfVersion.PDF_2_0, VersionConforming.DEPRECATED_NEED_APPEARANCES_IN_ACROFORM)) {
getPdfObject().remove(PdfName.NeedAppearances);
setModified();
} else {
put(PdfName.NeedAppearances, PdfBoolean.valueOf(needAppearances));
}
return this;
}
/**
* Gets the NeedAppearances
boolean property on the AcroForm.
* NeedAppearances has been deprecated in PDF 2.0.
*
*
* NeedAppearances is a flag specifying whether to construct appearance
* streams and appearance dictionaries for all widget annotations in the
* document.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @return the NeedAppearances
property as a {@link PdfBoolean}. Default value is false
*/
public PdfBoolean getNeedAppearances() {
return getPdfObject().getAsBoolean(PdfName.NeedAppearances);
}
/**
* Sets the SigFlags
integer property on the AcroForm.
*
*
* SigFlags is a set of flags specifying various document-level
* characteristics related to signature fields.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @param sigFlags an integer. Use {@link #SIGNATURE_EXIST} and/or {@link #APPEND_ONLY}.
* Use bitwise OR operator to combine these values. Default value is 0
* @return current AcroForm.
*/
public PdfAcroForm setSignatureFlags(int sigFlags) {
return put(PdfName.SigFlags, new PdfNumber(sigFlags));
}
/**
* Changes the SigFlags
integer property on the AcroForm.
* This method allows only to add flags, not to remove them.
*
*
* SigFlags is a set of flags specifying various document-level
* characteristics related to signature fields.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @param sigFlag an integer. Use {@link #SIGNATURE_EXIST} and/or {@link #APPEND_ONLY}.
* Use bitwise OR operator to combine these values. Default is 0
* @return current AcroForm.
*/
public PdfAcroForm setSignatureFlag(int sigFlag) {
int flags = getSignatureFlags();
flags = flags | sigFlag;
return setSignatureFlags(flags);
}
/**
* Gets the SigFlags
integer property on the AcroForm.
*
*
* SigFlags is a set of flags specifying various document-level
* characteristics related to signature fields
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @return current value for SigFlags
.
*/
public int getSignatureFlags() {
PdfNumber f = getPdfObject().getAsNumber(PdfName.SigFlags);
if (f == null) {
return 0;
} else {
return f.intValue();
}
}
/**
* Sets the CO
array property on the AcroForm.
*
*
* CO
, Calculation Order, is an array of indirect references to
* field dictionaries with calculation actions, defining the calculation
* order in which their values will be recalculated when the value of any
* field changes
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @param calculationOrder an array of indirect references
* @return current AcroForm
*/
public PdfAcroForm setCalculationOrder(PdfArray calculationOrder) {
return put(PdfName.CO, calculationOrder);
}
/**
* Gets the CO
array property on the AcroForm.
*
*
* CO
, Calculation Order, is an array of indirect references to
* field dictionaries with calculation actions, defining the calculation
* order in which their values will be recalculated when the value of any
* field changes
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @return an array of indirect references
*/
public PdfArray getCalculationOrder() {
return getPdfObject().getAsArray(PdfName.CO);
}
/**
* Sets the DR
dictionary property on the AcroForm.
*
*
* DR
is a resource dictionary containing default resources
* (such as fonts, patterns, or colour spaces) that shall be used by form
* field appearance streams. At a minimum, this dictionary shall contain a
* Font entry specifying the resource name and font dictionary of the
* default font for displaying text.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @param defaultResources a resource dictionary
* @return current AcroForm
*/
public PdfAcroForm setDefaultResources(PdfDictionary defaultResources) {
return put(PdfName.DR, defaultResources);
}
/**
* Gets the DR
dictionary property on the AcroForm.
*
*
* DR
is a resource dictionary containing default resources
* (such as fonts, patterns, or colour spaces) that shall be used by form
* field appearance streams. At a minimum, this dictionary shall contain a
* Font entry specifying the resource name and font dictionary of the
* default font for displaying text.
* (ISO 32000-1, section 12.7.2 "Interactive Form Dictionary")
*
*
* @return a resource dictionary
*/
public PdfDictionary getDefaultResources() {
return getPdfObject().getAsDictionary(PdfName.DR);
}
/**
* Sets the DA
String property on the AcroForm.
*
* This method sets a default (fallback value) for the DA
* attribute of variable text {@link PdfFormField form field}s.
*
* @param appearance a String containing a sequence of valid PDF syntax
* @return current AcroForm
*/
public PdfAcroForm setDefaultAppearance(String appearance) {
return put(PdfName.DA, new PdfString(appearance));
}
/**
* Gets the DA
String property on the AcroForm.
*
* This method returns the default (fallback value) for the DA
* attribute of variable text {@link PdfFormField form field}s.
*
* @return the form-wide default appearance, as a String
*/
public PdfString getDefaultAppearance() {
return getPdfObject().getAsString(PdfName.DA);
}
/**
* Sets the Q
integer property on the AcroForm.
*
* This method sets a default (fallback value) for the Q
* attribute of variable text {@link PdfFormField form field}s.
*
* @param justification an integer representing a justification value
* @return current AcroForm
* @see PdfFormField#setJustification(com.itextpdf.layout.properties.TextAlignment)
*/
public PdfAcroForm setDefaultJustification(int justification) {
return put(PdfName.Q, new PdfNumber(justification));
}
/**
* Gets the Q
integer property on the AcroForm.
*
* This method gets the default (fallback value) for the Q
* attribute of variable text {@link PdfFormField form field}s.
*
* @return an integer representing a justification value
* @see PdfFormField#getJustification()
*/
public PdfNumber getDefaultJustification() {
return getPdfObject().getAsNumber(PdfName.Q);
}
/**
* Sets the XFA
property on the AcroForm.
*
* XFA
can either be a {@link PdfStream} or a {@link PdfArray}.
* Its contents must be valid XFA.
*
* @param xfaResource a stream containing the XDP
* @return current AcroForm
*/
public PdfAcroForm setXFAResource(PdfStream xfaResource) {
return put(PdfName.XFA, xfaResource);
}
/**
* Sets the XFA
property on the AcroForm.
*
* XFA
can either be a {@link PdfStream} or a {@link PdfArray}.
* Its contents must be valid XFA.
*
* @param xfaResource an array of text string and stream pairs representing
* the individual packets comprising the XML Data Package. (ISO 32000-1,
* section 12.7.2 "Interactive Form Dictionary")
* @return current AcroForm
*/
public PdfAcroForm setXFAResource(PdfArray xfaResource) {
return put(PdfName.XFA, xfaResource);
}
/**
* Gets the XFA
property on the AcroForm.
*
* @return an object representing the entire XDP. It can either be a
* {@link PdfStream} or a {@link PdfArray}.
*/
public PdfObject getXFAResource() {
return getPdfObject().get(PdfName.XFA);
}
/**
* Gets a {@link PdfFormField form field} by its name.
*
* @param fieldName the name of the {@link PdfFormField form field} to retrieve
* @return the {@link PdfFormField form field}, or null
if it
* isn't present
*/
public PdfFormField getField(String fieldName) {
if (fields.get(fieldName) != null) {
return fields.get(fieldName);
}
final String[] splitFieldsArray = StringSplitUtil.splitKeepTrailingWhiteSpace(fieldName, '.');
if (splitFieldsArray.length == 0) {
return null;
}
PdfFormField parentFormField = fields.get(splitFieldsArray[0]);
PdfFormField kidField = parentFormField;
for (int i = 1; i < splitFieldsArray.length; i++) {
if (parentFormField == null || parentFormField.isFlushed()) {
return null;
}
kidField = parentFormField.getChildField(splitFieldsArray[i]);
parentFormField = kidField;
}
return kidField;
}
/**
* Gets the attribute generateAppearance, which tells {@link #flattenFields()}
* to generate an appearance Stream for all {@link PdfFormField form field}s
* that don't have one.
*
* @return bolean value indicating if the appearances need to be generated
*/
public boolean isGenerateAppearance() {
return generateAppearance;
}
/**
* Sets the attribute generateAppearance, which tells {@link #flattenFields()}
* to generate an appearance Stream for all {@link PdfFormField form field}s
* that don't have one.
*
* Not generating appearances will speed up form flattening but the results
* can be unexpected in Acrobat. Don't use it unless your environment is
* well controlled. The default is true
.
*
* If generateAppearance is set to true
, then
* NeedAppearances
is set to false
. This does not
* apply vice versa.
*
* Note, this method does not change default behaviour of {@link PdfFormField#setValue(String)} method.
*
* @param generateAppearance a boolean
*/
public void setGenerateAppearance(boolean generateAppearance) {
if (generateAppearance) {
getPdfObject().remove(PdfName.NeedAppearances);
setModified();
}
this.generateAppearance = generateAppearance;
}
/**
* Flattens interactive {@link PdfFormField form field}s in the document. If
* no fields have been explicitly included via {@link #partialFormFlattening},
* then all fields are flattened. Otherwise only the included fields are
* flattened.
*/
public void flattenFields() {
if (document.isAppendMode()) {
throw new PdfException(FormsExceptionMessageConstant.FIELD_FLATTENING_IS_NOT_SUPPORTED_IN_APPEND_MODE);
}
Set fields;
if (fieldsForFlattening.isEmpty()) {
this.fields.clear();
fields = getAllFormFieldsWithoutNames();
} else {
fields = new LinkedHashSet<>();
for (PdfFormField field : fieldsForFlattening) {
fields.addAll(prepareFieldsForFlattening(field));
}
}
// In case of appearance resources and page resources are the same object, it would not be possible to add
// the xObject to the page resources. So in that case we would copy page resources and use the copy for
// xObject, so that circular reference is avoided.
// We copy beforehand firstly not to produce a copy every time, and secondly not to copy all the
// xObjects that have already been added to the page resources.
Map initialPageResourceClones = new LinkedHashMap<>();
for (int i = 1; i <= document.getNumberOfPages(); i++) {
PdfObject resources = document.getPage(i).getPdfObject().getAsDictionary(PdfName.Resources);
initialPageResourceClones.put(i, resources == null ? null : resources.clone());
}
Set wrappedPages = new LinkedHashSet<>();
PdfPage page;
for (PdfFormField formField : fields) {
for (PdfFormAnnotation fieldAnnot: formField.getChildFormAnnotations()) {
final PdfDictionary fieldObject = fieldAnnot.getPdfObject();
page = getFieldPage(fieldObject);
if (page == null) {
continue;
}
final PdfAnnotation annotation = PdfAnnotation.makeAnnotation(fieldObject);
TagTreePointer tagPointer = null;
if (annotation != null && document.isTagged()) {
tagPointer = document.getTagStructureContext().removeAnnotationTag(annotation);
}
PdfDictionary appDic = fieldObject.getAsDictionary(PdfName.AP);
PdfObject asNormal = null;
if (appDic != null) {
asNormal = appDic.getAsStream(PdfName.N);
if (asNormal == null) {
asNormal = appDic.getAsDictionary(PdfName.N);
}
}
if (generateAppearance) {
if (appDic == null || asNormal == null) {
fieldAnnot.regenerateField();
appDic = fieldObject.getAsDictionary(PdfName.AP);
}
}
PdfObject normal = appDic != null ? appDic.get(PdfName.N) : null;
if (null != normal) {
PdfFormXObject xObject = null;
if (normal.isStream()) {
xObject = new PdfFormXObject((PdfStream) normal);
} else if (normal.isDictionary()) {
PdfName as = fieldObject.getAsName(PdfName.AS);
if (((PdfDictionary) normal).getAsStream(as) != null) {
xObject = new PdfFormXObject(((PdfDictionary) normal).getAsStream(as));
xObject.makeIndirect(document);
}
}
if (xObject != null) {
//subtype is required field for FormXObject, but can be omitted in normal appearance.
xObject.put(PdfName.Subtype, PdfName.Form);
Rectangle annotBBox = fieldObject.getAsRectangle(PdfName.Rect);
if (page.isFlushed()) {
throw new PdfException(
FormsExceptionMessageConstant.PAGE_ALREADY_FLUSHED_USE_ADD_FIELD_APPEARANCE_TO_PAGE_METHOD_BEFORE_PAGE_FLUSHING);
}
PdfCanvas canvas = new PdfCanvas(page, !wrappedPages.contains(page));
wrappedPages.add(page);
// Here we avoid circular reference which might occur when page resources and the appearance xObject's
// resources are the same object
PdfObject xObjectResources = xObject.getPdfObject().get(PdfName.Resources);
PdfObject pageResources = page.getResources().getPdfObject();
if (xObjectResources != null && xObjectResources == pageResources) {
xObject.getPdfObject().put(PdfName.Resources,
initialPageResourceClones.get(document.getPageNumber(page)));
}
if (tagPointer != null) {
tagPointer.setPageForTagging(page);
TagReference tagRef = tagPointer.getTagReference();
canvas.openTag(tagRef);
}
AffineTransform at = calcFieldAppTransformToAnnotRect(xObject, annotBBox);
float[] m = new float[6];
at.getMatrix(m);
canvas.addXObjectWithTransformationMatrix(xObject, m[0], m[1], m[2], m[3], m[4], m[5]);
if (tagPointer != null) {
canvas.closeTag();
}
}
} else {
LOGGER.warn(FormsLogMessageConstants.N_ENTRY_IS_REQUIRED_FOR_APPEARANCE_DICTIONARY);
}
PdfArray fFields = getFields();
if (annotation != null) {
page.removeAnnotation(annotation);
}
removeFieldFromParentAndAcroForm(fFields, fieldObject);
}
}
getPdfObject().remove(PdfName.NeedAppearances);
if (fieldsForFlattening.size() == 0) {
getFields().clear();
}
if (getFields().isEmpty()) {
document.getCatalog().remove(PdfName.AcroForm);
}
}
/**
* Tries to remove the {@link PdfFormField form field} with the specified
* name from the document.
*
* @param fieldName the name of the {@link PdfFormField form field} to remove
* @return a boolean representing whether or not the removal succeeded.
*/
public boolean removeField(String fieldName) {
PdfFormField field = getField(fieldName);
if (field == null) {
return false;
}
PdfDictionary fieldObject = field.getPdfObject();
PdfPage page = getFieldPage(fieldObject);
PdfAnnotation annotation = PdfAnnotation.makeAnnotation(fieldObject);
if (page != null && annotation != null) {
page.removeAnnotation(annotation);
}
PdfDictionary parent = field.getParent();
PdfFormField parentField = field.getParentField();
if (parent != null) {
PdfArray kids = parent.getAsArray(PdfName.Kids);
if (parentField != null) {
parentField.removeChild(field);
}
kids.remove(fieldObject);
kids.setModified();
parent.setModified();
return true;
}
PdfArray fieldsPdfArray = getFields();
if (fieldsPdfArray.contains(fieldObject)) {
fieldsPdfArray.remove(fieldObject);
this.fields.remove(fieldName);
fieldsPdfArray.setModified();
setModified();
return true;
}
return false;
}
/**
* Adds a {@link PdfFormField form field}, identified by name, to the list of fields to be flattened.
* Does not perform a flattening operation in itself.
*
* @param fieldName the name of the {@link PdfFormField form field} to be flattened
*/
public void partialFormFlattening(String fieldName) {
PdfFormField field = getAllFormFields().get(fieldName);
if (field != null) {
fieldsForFlattening.add(field);
}
}
/**
* Changes the identifier of a {@link PdfFormField form field}.
*
* @param oldName the current name of the field
* @param newName the new name of the field. Must not be used currently.
*/
public void renameField(String oldName, String newName) {
final PdfFormField oldField = getField(oldName);
if (oldField == null) {
LOGGER.warn(MessageFormatUtil.format(
FormsLogMessageConstants.FIELDNAME_NOT_FOUND_OPERATION_CAN_NOT_BE_COMPLETED, oldName));
return;
}
getField(oldName).setFieldName(newName);
PdfFormField field = fields.get(oldName);
if (field != null) {
fields.remove(oldName);
fields.put(newName, field);
}
}
/**
* Creates an in-memory copy of a {@link PdfFormField}. This new field is
* not added to the document.
*
* @param name the name of the {@link PdfFormField form field} to be copied
* @return a clone of the original {@link PdfFormField}
*/
public PdfFormField copyField(String name) {
PdfFormField oldField = getField(name);
if (oldField != null) {
return PdfFormCreator.createFormField(
(PdfDictionary) oldField.getPdfObject().clone().makeIndirect(document));
}
return null;
}
/**
* Replaces the {@link PdfFormField} of a certain name with another
* {@link PdfFormField}.
*
* @param name the name of the {@link PdfFormField form field} to be replaced
* @param field the new {@link PdfFormField}
*/
public void replaceField(String name, PdfFormField field) {
if (name == null) {
LOGGER.warn(FormsLogMessageConstants.PROVIDE_FORMFIELD_NAME);
return;
}
removeField(name);
final int lastDotIndex = name.lastIndexOf('.');
if (lastDotIndex == -1) {
addField(field);
return;
}
final String parentName = name.substring(0, lastDotIndex);
final PdfFormField parent = getField(parentName);
if (parent == null) {
addField(field);
} else {
parent.addKid(field);
}
}
/**
* Disables appearance stream regeneration for all the root fields in the Acroform, so all of its children
* in the hierarchy will also not be regenerated.
*/
public void disableRegenerationForAllFields() {
for (PdfFormField rootField : getRootFormFields().values()) {
rootField.disableFieldRegeneration();
}
}
/**
* Enables appearance stream regeneration for all the fields in the Acroform and regenerates them.
*/
public void enableRegenerationForAllFields() {
for (PdfFormField rootField : getRootFormFields().values()) {
rootField.enableFieldRegeneration();
}
}
/**
* Gets all AcroForm fields in the document.
*
* @return a {@link PdfArray} of field dictionaries
*/
protected PdfArray getFields() {
PdfArray fields = getPdfObject().getAsArray(PdfName.Fields);
if (fields == null) {
LOGGER.warn(FormsLogMessageConstants.NO_FIELDS_IN_ACROFORM);
fields = new PdfArray();
getPdfObject().put(PdfName.Fields, fields);
}
return fields;
}
@Override
protected boolean isWrappedObjectMustBeIndirect() {
return false;
}
private Map populateFormFieldsMap() {
final PdfArray rawFields = getFields();
Map fields = new LinkedHashMap<>();
final PdfArray shouldBeRemoved = new PdfArray();
for (PdfObject field : rawFields) {
if (field.isFlushed()) {
LOGGER.info(FormsLogMessageConstants.FORM_FIELD_WAS_FLUSHED);
continue;
}
PdfFormField formField = PdfFormField.makeFormField(field, document);
if (formField == null) {
// Pure annotation can't be in AcroForm dictionary
// Ok, let's just skip them, they were (will be) processed with their parents if any
LOGGER.warn(FormsLogMessageConstants.ANNOTATION_IN_ACROFORM_DICTIONARY);
continue;
}
PdfFormFieldMergeUtil.mergeKidsWithSameNames(formField, false);
PdfString fieldName = formField.getFieldName();
if (fieldName != null) {
String name = formField.getFieldName().toUnicodeString();
if (formField.isInReadingMode() || !fields.containsKey(name) ||
!PdfFormFieldMergeUtil.mergeTwoFieldsWithTheSameNames(fields.get(name), formField, true)) {
fields.put(formField.getFieldName().toUnicodeString(), formField);
} else {
shouldBeRemoved.add(field);
}
}
}
for (PdfObject field : shouldBeRemoved) {
rawFields.remove(field);
}
return fields;
}
private void removeFieldFromParentAndAcroForm(PdfArray formFields, PdfDictionary fieldObject) {
formFields.remove(fieldObject);
PdfDictionary parent = fieldObject.getAsDictionary(PdfName.Parent);
if (parent != null) {
PdfArray kids = parent.getAsArray(PdfName.Kids);
if (kids == null) {
formFields.remove(parent);
} else {
kids.remove(fieldObject);
if (kids.isEmpty()) {
removeFieldFromParentAndAcroForm(formFields, parent);
}
}
}
}
private void processKids(PdfFormField field, PdfPage page) {
PdfArray kids = field.getKids();
if (kids == null) {
return;
}
if (kids.size() == 1) {
PdfDictionary kidDict = (PdfDictionary) kids.get(0);
PdfName type = kidDict.getAsName(PdfName.Subtype);
if (PdfName.Widget.equals(type)) {
if (PdfFormAnnotationUtil.isPureWidget(kidDict)) {
// kid is not merged field with widget
PdfFormAnnotationUtil.mergeWidgetWithParentField(field);
defineWidgetPageAndAddToIt(page, field.getPdfObject(), true);
} else {
defineWidgetPageAndAddToIt(page, kidDict, true);
}
return;
}
}
for (AbstractPdfFormField child : field.getChildFields()) {
if (PdfFormAnnotationUtil.isPureWidgetOrMergedField(child.getPdfObject())) {
defineWidgetPageAndAddToIt(page, child.getPdfObject(), true);
} else if (child instanceof PdfFormField) {
processKids((PdfFormField)child, page);
}
}
}
private void defineWidgetPageAndAddToIt(PdfPage currentPage, PdfDictionary mergedFieldAndWidget, boolean warnIfPageFlushed) {
PdfAnnotation annot = PdfAnnotation.makeAnnotation(mergedFieldAndWidget);
PdfPage page = getFieldPage(mergedFieldAndWidget);
if (page != null) {
PdfFormAnnotationUtil.addWidgetAnnotationToPage(page, annot);
return;
}
PdfDictionary pageDic = annot.getPageObject();
if (pageDic == null) {
PdfFormAnnotationUtil.addWidgetAnnotationToPage(currentPage, annot);
} else {
if (warnIfPageFlushed && pageDic.isFlushed()) {
throw new PdfException(
FormsExceptionMessageConstant.PAGE_ALREADY_FLUSHED_USE_ADD_FIELD_APPEARANCE_TO_PAGE_METHOD_BEFORE_PAGE_FLUSHING);
}
PdfDocument doc = pageDic.getIndirectReference().getDocument();
PdfPage widgetPage = doc.getPage(pageDic);
PdfFormAnnotationUtil.addWidgetAnnotationToPage(widgetPage == null ? currentPage : widgetPage, annot);
}
}
/**
* Determines whether the AcroForm contains XFA data.
*
* @return a boolean
*/
public boolean hasXfaForm() {
return xfaForm != null && xfaForm.isXfaPresent();
}
/**
* Gets the {@link XfaForm} atribute.
*
* @return the XFA form object
*/
public XfaForm getXfaForm() {
return xfaForm;
}
/**
* Removes the XFA stream from the document.
*/
public void removeXfaForm() {
if (hasXfaForm()) {
PdfDictionary root = document.getCatalog().getPdfObject();
PdfDictionary acroform = root.getAsDictionary(PdfName.AcroForm);
acroform.remove(PdfName.XFA);
xfaForm = null;
}
}
/**
* Put a key/value pair in the dictionary and overwrite previous value if it already exists.
*
* @param key the key as pdf name
* @param value the value as pdf object
*
* @return this {@link PdfAcroForm} instance
*/
public PdfAcroForm put(PdfName key, PdfObject value) {
getPdfObject().put(key, value);
setModified();
return this;
}
/**
* Releases underlying pdf object and other pdf entities used by wrapper.
* This method should be called instead of direct call to {@link PdfObject#release()} if the wrapper is used.
*/
public void release() {
unsetForbidRelease();
getPdfObject().release();
if (fields != null) {
for (PdfFormField field : fields.values()) {
field.release();
}
fields.clear();
fields = null;
}
}
@Override
public PdfObjectWrapper setModified() {
if (getPdfObject().getIndirectReference() != null) {
super.setModified();
} else {
document.getCatalog().setModified();
}
return this;
}
private static PdfDictionary createAcroFormDictionaryByFields(PdfArray fields) {
PdfDictionary dictionary = new PdfDictionary();
dictionary.put(PdfName.Fields, fields);
return dictionary;
}
private PdfPage getFieldPage(PdfDictionary annotDict) {
PdfDictionary pageDic = annotDict.getAsDictionary(PdfName.P);
if (pageDic != null) {
return document.getPage(pageDic);
}
for (int i = 1; i <= document.getNumberOfPages(); i++) {
PdfPage page = document.getPage(i);
if (!page.isFlushed()) {
PdfAnnotation annotation = PdfAnnotation.makeAnnotation(annotDict);
if (annotation != null && page.containsAnnotation(annotation)) {
return page;
}
}
}
return null;
}
private Set prepareFieldsForFlattening(PdfFormField field) {
Set preparedFields = new LinkedHashSet<>();
preparedFields.add(field);
for (PdfFormField child : field.getChildFormFields()) {
preparedFields.addAll(prepareFieldsForFlattening(child));
}
return preparedFields;
}
private AffineTransform calcFieldAppTransformToAnnotRect(PdfFormXObject xObject, Rectangle annotBBox) {
PdfArray bBox = xObject.getBBox();
if (bBox.size() != 4) {
bBox = new PdfArray(new Rectangle(0, 0));
xObject.setBBox(bBox);
}
float[] xObjBBox = bBox.toFloatArray();
PdfArray xObjMatrix = xObject.getPdfObject().getAsArray(PdfName.Matrix);
Rectangle transformedRect;
if (xObjMatrix != null && xObjMatrix.size() == 6) {
Point[] xObjRectPoints = new Point[]{
new Point(xObjBBox[0], xObjBBox[1]),
new Point(xObjBBox[0], xObjBBox[3]),
new Point(xObjBBox[2], xObjBBox[1]),
new Point(xObjBBox[2], xObjBBox[3])
};
Point[] transformedAppBoxPoints = new Point[xObjRectPoints.length];
new AffineTransform(xObjMatrix.toDoubleArray()).transform(xObjRectPoints, 0, transformedAppBoxPoints, 0, xObjRectPoints.length);
float[] transformedRectArr = new float[] {
Float.MAX_VALUE, Float.MAX_VALUE,
-Float.MAX_VALUE, -Float.MAX_VALUE,
};
for (Point p : transformedAppBoxPoints) {
transformedRectArr[0] = (float) Math.min(transformedRectArr[0], p.x);
transformedRectArr[1] = (float) Math.min(transformedRectArr[1], p.y);
transformedRectArr[2] = (float) Math.max(transformedRectArr[2], p.x);
transformedRectArr[3] = (float) Math.max(transformedRectArr[3], p.y);
}
transformedRect = new Rectangle(transformedRectArr[0], transformedRectArr[1], transformedRectArr[2] - transformedRectArr[0], transformedRectArr[3] - transformedRectArr[1]);
} else {
transformedRect = new Rectangle(0, 0).setBbox(xObjBBox[0], xObjBBox[1], xObjBBox[2], xObjBBox[3]);
}
AffineTransform at = AffineTransform.getTranslateInstance(-transformedRect.getX(), -transformedRect.getY());
float scaleX = transformedRect.getWidth() == 0 ? 1 : annotBBox.getWidth() / transformedRect.getWidth();
float scaleY = transformedRect.getHeight() == 0 ? 1 : annotBBox.getHeight() / transformedRect.getHeight();
at.preConcatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
at.preConcatenate(AffineTransform.getTranslateInstance(annotBBox.getX(), annotBBox.getY()));
return at;
}
private Set getAllFormFieldsWithoutNames() {
if (fields.isEmpty()) {
fields = populateFormFieldsMap();
}
Set allFields = new LinkedHashSet<>();
for (Entry field : fields.entrySet()) {
allFields.add(field.getValue());
List kids = field.getValue().getAllChildFormFields();
allFields.addAll(kids);
}
return allFields;
}
private boolean needToAddToAcroform(PdfFormField field, boolean throwExceptionOnError) {
final String fieldNameBeforeMergeCall = field.getFieldName().toUnicodeString();
if (!fields.containsKey(fieldNameBeforeMergeCall)) {
return true;
}
if (!PdfFormFieldMergeUtil.mergeTwoFieldsWithTheSameNames(fields.get(fieldNameBeforeMergeCall), field,
throwExceptionOnError)) {
return true;
}
final boolean isFieldNameChanged = !fieldNameBeforeMergeCall.equals(field.getFieldName().toUnicodeString());
return isFieldNameChanged;
}
}