All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.forms.PdfAcroForm Maven / Gradle / Ivy

There is a newer version: 8.0.5
Show newest version
/*
    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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy