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

com.itextpdf.forms.fields.PdfFormField Maven / Gradle / Ivy

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2023 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.fields;

import com.itextpdf.commons.datastructures.NullableContainer;
import com.itextpdf.commons.utils.Base64;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.properties.CheckBoxType;
import com.itextpdf.forms.logs.FormsLogMessageConstants;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.source.OutputStream;
import com.itextpdf.kernel.colors.Color;
import com.itextpdf.kernel.colors.DeviceCmyk;
import com.itextpdf.kernel.colors.DeviceGray;
import com.itextpdf.kernel.colors.DeviceRgb;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfIndirectReference;
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.PdfOutputStream;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.layout.properties.TextAlignment;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class represents a single field or field group in an {@link com.itextpdf.forms.PdfAcroForm
 * AcroForm}.
 *
 * 

* To be able to be wrapped with this {@link PdfObjectWrapper} the {@link PdfObject} * must be indirect. */ public class PdfFormField extends AbstractPdfFormField { /** * Flag that designates, if set, that the field can contain multiple lines * of text. */ public static final int FF_MULTILINE = makeFieldFlag(13); /** * Flag that designates, if set, that the field's contents must be obfuscated. */ public static final int FF_PASSWORD = makeFieldFlag(14); /** * The ReadOnly flag, which specifies whether or not the field can be changed. */ public static final int FF_READ_ONLY = makeFieldFlag(1); /** * The Required flag, which specifies whether or not the field must be filled in. */ public static final int FF_REQUIRED = makeFieldFlag(2); /** * The NoExport flag, which specifies whether or not exporting is forbidden. */ public static final int FF_NO_EXPORT = makeFieldFlag(3); /** * List of all allowable keys in form fields. */ private static final Set FORM_FIELD_KEYS = new HashSet<>(); private static final Logger LOGGER = LoggerFactory.getLogger(PdfFormField.class); protected String text; protected ImageData img; protected PdfFormXObject form; protected NullableContainer checkType = null; private String displayValue; private List childFields = new ArrayList<>(); static { FORM_FIELD_KEYS.add(PdfName.FT); // It exists in form field and widget annotation //formFieldKeys.add(PdfName.Parent); FORM_FIELD_KEYS.add(PdfName.Kids); FORM_FIELD_KEYS.add(PdfName.T); FORM_FIELD_KEYS.add(PdfName.TU); FORM_FIELD_KEYS.add(PdfName.TM); FORM_FIELD_KEYS.add(PdfName.Ff); FORM_FIELD_KEYS.add(PdfName.V); FORM_FIELD_KEYS.add(PdfName.DV); // It exists in form field and widget annotation //formFieldKeys.add(PdfName.AA); FORM_FIELD_KEYS.add(PdfName.DA); FORM_FIELD_KEYS.add(PdfName.Q); FORM_FIELD_KEYS.add(PdfName.DS); FORM_FIELD_KEYS.add(PdfName.RV); FORM_FIELD_KEYS.add(PdfName.Opt); FORM_FIELD_KEYS.add(PdfName.MaxLen); FORM_FIELD_KEYS.add(PdfName.TI); FORM_FIELD_KEYS.add(PdfName.I); FORM_FIELD_KEYS.add(PdfName.Lock); FORM_FIELD_KEYS.add(PdfName.SV); } /** * Creates a form field as a wrapper object around a {@link PdfDictionary}. * This {@link PdfDictionary} must be an indirect object. * * @param pdfObject the dictionary to be wrapped, must have an indirect reference. */ public PdfFormField(PdfDictionary pdfObject) { super(pdfObject); createKids(pdfObject); } private void createKids(PdfDictionary pdfObject) { PdfArray kidsArray = pdfObject.getAsArray(PdfName.Kids); if (kidsArray == null) { // Here widget annotation might be merged with form field final PdfName subType = pdfObject.getAsName(PdfName.Subtype); if (PdfName.Widget.equals(subType)) { AbstractPdfFormField childField = PdfFormAnnotation.makeFormAnnotation(pdfObject, getDocument()); if (childField != null) { this.setChildField(childField); } } } else { for (PdfObject kid : kidsArray) { if (kid.isFlushed()) { LOGGER.info(FormsLogMessageConstants.FORM_FIELD_WAS_FLUSHED); continue; } AbstractPdfFormField childField = PdfFormField.makeFormFieldOrAnnotation(kid, getDocument()); if (childField != null) { this.setChildField(childField); } else { LOGGER.warn(MessageFormatUtil.format(FormsLogMessageConstants.CANNOT_CREATE_FORMFIELD, pdfObject.getIndirectReference() == null ? pdfObject : (PdfObject) pdfObject.getIndirectReference())); } } } } /** * Creates a minimal {@link PdfFormField}. * * @param pdfDocument The {@link PdfDocument} instance. */ protected PdfFormField(PdfDocument pdfDocument) { this((PdfDictionary) new PdfDictionary().makeIndirect(pdfDocument)); PdfName formType = getFormType(); if (formType != null) { put(PdfName.FT, formType); } } /** * Creates a form field as a parent of a {@link PdfWidgetAnnotation}. * * @param widget The widget which will be a kid of the {@link PdfFormField}. * @param pdfDocument The {@link PdfDocument} instance. */ protected PdfFormField(PdfWidgetAnnotation widget, PdfDocument pdfDocument) { this((PdfDictionary) new PdfDictionary().makeIndirect(pdfDocument)); widget.makeIndirect(pdfDocument); addKid(widget); if (getFormType() != null) { put(PdfName.FT, getFormType()); } } /** * Creates a (subtype of) {@link PdfFormField} object. The type of the object * depends on the FT entry in the pdfObject parameter. * * @param pdfObject assumed to be either a {@link PdfDictionary}, or a * {@link PdfIndirectReference} to a {@link PdfDictionary}. * @param document the {@link PdfDocument} to create the field in. * @return a new {@link PdfFormField}, or null if * pdfObject is not a form field. */ public static PdfFormField makeFormField(PdfObject pdfObject, PdfDocument document) { if (!pdfObject.isDictionary()) { return null; } PdfDictionary dictionary = (PdfDictionary) pdfObject; if (!PdfFormField.isFormField(dictionary)) { return null; } PdfFormField field; PdfName formType = dictionary.getAsName(PdfName.FT); if (PdfName.Tx.equals(formType)) { field = PdfFormCreator.createTextFormField(dictionary); } else if (PdfName.Btn.equals(formType)) { field = PdfFormCreator.createButtonFormField(dictionary); } else if (PdfName.Ch.equals(formType)) { field = PdfFormCreator.createChoiceFormField(dictionary); } else if (PdfName.Sig.equals(formType)) { field = PdfFormCreator.createSignatureFormField(dictionary); } else { // No form type but still a form field field = PdfFormCreator.createFormField(dictionary); } field.makeIndirect(document); if (document != null && document.getReader() != null && document.getReader().getPdfAConformanceLevel() != null) { field.pdfAConformanceLevel = document.getReader().getPdfAConformanceLevel(); } return field; } /** * Creates a (subtype of) {@link PdfFormField} or {@link PdfFormAnnotation} object depending on * pdfObject. * * @param pdfObject assumed to be either a {@link PdfDictionary}, or a * {@link PdfIndirectReference} to a {@link PdfDictionary}. * @param document the {@link PdfDocument} to create the field in. * @return a new {@link AbstractPdfFormField}, or null if * pdfObject is not a form field and is not a widget annotation. */ public static AbstractPdfFormField makeFormFieldOrAnnotation(PdfObject pdfObject, PdfDocument document) { AbstractPdfFormField formField = PdfFormField.makeFormField(pdfObject, document); if (formField == null) { formField = PdfFormAnnotation.makeFormAnnotation(pdfObject, document); } return formField; } /** * Makes a field flag by bit position. Bit positions are numbered 1 to 32. * But position 0 corresponds to flag 1, position 3 corresponds to flag 4 etc. * * @param bitPosition bit position of a flag in range 1 to 32 from the pdf specification. * @return corresponding field flag. */ public static int makeFieldFlag(int bitPosition) { return (1 << (bitPosition - 1)); } /** * Checks if dictionary contains any of the form field keys. * * @param dict field dictionary to check. * * @return true if it is a form field dictionary, false otherwise. */ public static boolean isFormField(PdfDictionary dict) { for (final PdfName formFieldKey : getFormFieldKeys()) { if (dict.containsKey(formFieldKey)) { return true; } } return false; } /** * Gets a set of all possible form field keys except {@code PdfName.Parent}. * * @return a set of form field keys. */ public static Collection getFormFieldKeys() { return Collections.unmodifiableCollection(FORM_FIELD_KEYS); } /** * Returns the type of the form field dictionary, or of the parent * <PdfDictionary> object. * * @param fieldDict field dictionary to get its type. * * @return the form type, as a {@link PdfName}. */ public static PdfName getFormType(PdfDictionary fieldDict) { PdfName formType = fieldDict.getAsName(PdfName.FT); if (formType == null) { return getTypeFromParent(fieldDict); } return formType; } /** * Returns the type of the parent form field, or of the wrapped * <PdfDictionary> object. * * @return the form type, as a {@link PdfName}. */ public PdfName getFormType() { return getFormType(getPdfObject()); } /** * Sets a value to the field and generating field appearance if needed. * * @param value of the field. * @return the field. */ public PdfFormField setValue(String value) { PdfName formType = getFormType(); boolean autoGenerateAppearance = !(PdfName.Btn.equals(formType) && getFieldFlag(PdfButtonFormField.FF_RADIO)); return setValue(value, autoGenerateAppearance); } /** * Sets a value to the field (and fields with the same names) and generates field appearance if needed. * * @param value of the field. * @param generateAppearance if false, appearance won't be regenerated. * @return the field. */ public PdfFormField setValue(String value, boolean generateAppearance) { if (parent == null) { setFieldValue(value, generateAppearance); } else { String fieldName = getPartialFieldName().toUnicodeString(); for (PdfFormField field : parent.getChildFormFields()) { if (fieldName.equals(field.getPartialFieldName().toUnicodeString())) { field.setFieldValue(value, generateAppearance); } } } return this; } /** * Set text field value with given font and size. * * @param value text value. * @param font a {@link PdfFont}. * @param fontSize the size of the font. * @return the edited field. */ public PdfFormField setValue(String value, PdfFont font, float fontSize) { updateFontAndFontSize(font, fontSize); return setValue(value); } /** * Sets the field value and the display string. The display string * is used to build the appearance. * * @param value the field value. * @param displayValue the string that is used for the appearance. If null * the value parameter will be used. * @return the edited field. */ public PdfFormField setValue(String value, String displayValue) { if (value == null) { LOGGER.warn(FormsLogMessageConstants.FIELD_VALUE_CANNOT_BE_NULL); return this; } // Not valid for checkboxes and radiobuttons // TODO: DEVSIX-6344 - Move specific methods to related form fields classes if (displayValue == null || displayValue.equals(value)) { return setValue(value); } setValue(displayValue, true); setValue(value, false); this.displayValue = displayValue; return this; } /** * Removes the childField object of this field. * * @param fieldName a {@link PdfFormField}, that needs to be removed from form field children. */ public void removeChild(AbstractPdfFormField fieldName) { childFields.remove(fieldName); PdfArray kids = getPdfObject().getAsArray(PdfName.Kids); if (kids != null) { kids.remove(fieldName.getPdfObject()); if (kids.isEmpty()) { getPdfObject().remove(PdfName.Kids); } } } /** * Removes all children from the current field. */ public void removeChildren() { childFields.clear(); getPdfObject().remove(PdfName.Kids); } /** * Gets the kids of this object. * * @return contents of the dictionary's Kids property, as a {@link PdfArray}. */ public PdfArray getKids() { return getPdfObject().getAsArray(PdfName.Kids); } /** * Gets the childFields of this object. * * @return the children of the current field. */ public List getChildFields() { return Collections.unmodifiableList(childFields); } /** * Gets all child form fields of this form field. Annotations are not returned. * * @return a list of {@link PdfFormField}. */ public List getChildFormFields() { List fields = new ArrayList<>(); for (AbstractPdfFormField child : childFields) { if (child instanceof PdfFormField) { fields.add((PdfFormField)child); } } return fields; } /** * Gets all childFields of this object, including the children of the children * but not annotations. * * @return the children of the current field and their children. */ public List getAllChildFormFields() { List allKids = new ArrayList<>(); List kids = this.getChildFormFields(); for (PdfFormField formField : kids) { allKids.add(formField); allKids.addAll(formField.getAllChildFormFields()); } return allKids; } /** * Gets all childFields of this object, including the children of the children. * * @return the children of the current field and their children. */ public List getAllChildFields() { List kids = this.getChildFields(); List allKids = new ArrayList<>(kids); for (AbstractPdfFormField field : kids) { if (field instanceof PdfFormField) { allKids.addAll(((PdfFormField)field).getAllChildFields()); } } return allKids; } /** * Gets the child field of form field. If there is no child field with such name, {@code null} is returned. * * @param fieldName a {@link String}, name of the received field. * @return the child of the current field as a {@link PdfFormField}. */ public PdfFormField getChildField(String fieldName) { for (PdfFormField formField : this.getChildFormFields()) { PdfString partialFieldName = formField.getPartialFieldName(); if (partialFieldName != null && partialFieldName.toUnicodeString().equals(fieldName)) { return formField; } } return null; } /** * Adds a new kid to the Kids array property from a * {@link AbstractPdfFormField}. Also sets the kid's Parent property to this object. * * @param kid a new {@link AbstractPdfFormField} entry for the field's Kids array property. * * @return the edited {@link PdfFormField}. */ public PdfFormField addKid(AbstractPdfFormField kid) { return addKid(kid, true); } /** * Adds a new kid to the Kids array property from a * {@link AbstractPdfFormField}. Also sets the kid's Parent property to this object. * * @param kid a new {@link AbstractPdfFormField} entry for the field's Kids array property. * @param throwExceptionOnError define whether exception (true) or log (false) is expected in case kid with * the same name exists and merge of two kids failed. * * @return the edited {@link PdfFormField}. */ public PdfFormField addKid(AbstractPdfFormField kid, boolean throwExceptionOnError) { PdfFormAnnotationUtil.separateWidgetAndField(this); kid.setParent(this); PdfArray kids = getKids(); if (kids == null) { kids = new PdfArray(); } if (!mergeKidsIfKidWithSuchNameExists(kid, throwExceptionOnError)) { kids.add(kid.getPdfObject()); this.childFields.add(kid); } put(PdfName.Kids, kids); return this; } /** * Adds a new kid to the Kids array property from a * {@link PdfWidgetAnnotation}. Also sets the kid's Parent property to this object. * * @param kid a new {@link PdfWidgetAnnotation} entry for the field's Kids array property. * @return the edited {@link PdfFormField}. */ public PdfFormField addKid(PdfWidgetAnnotation kid) { kid.setParent(getPdfObject()); PdfDictionary pdfObject = kid.getPdfObject(); pdfObject.makeIndirect(this.getDocument()); AbstractPdfFormField field = PdfFormCreator.createFormAnnotation(pdfObject); return addKid(field); } /** * Changes the name of the field to the specified value. * * @param name the new field name, as a String. * @return the edited {@link PdfFormField}. */ public PdfFormField setFieldName(String name) { put(PdfName.T, new PdfString(name)); PdfFormField parent = getParentField(); if (parent != null) { parent.mergeKidsIfKidWithSuchNameExists(this, true); } return this; } /** * Gets the current field partial name. * * @return the current field partial name, as a {@link PdfString}. If the field has no partial name, * an empty {@link PdfString} is returned. */ public PdfString getPartialFieldName() { PdfString partialName = getPdfObject().getAsString(PdfName.T); return partialName == null ? new PdfString("") : partialName; } /** * Changes the alternate name of the field to the specified value. The * alternate is a descriptive name to be used by status messages etc. * * @param name the new alternate name, as a String. * @return the edited {@link PdfFormField}. */ public PdfFormField setAlternativeName(String name) { put(PdfName.TU, new PdfString(name)); return this; } /** * Gets the current alternate name. The alternate is a descriptive name to * be used by status messages etc. * * @return the current alternate name, as a {@link PdfString}. */ public PdfString getAlternativeName() { return getPdfObject().getAsString(PdfName.TU); } /** * Changes the mapping name of the field to the specified value. The * mapping name can be used when exporting the form data in the document. * * @param name the new alternate name, as a String. * @return the edited field. */ public PdfFormField setMappingName(String name) { put(PdfName.TM, new PdfString(name)); return this; } /** * Gets the current mapping name. The mapping name can be used when * exporting the form data in the document. * * @return the current mapping name, as a {@link PdfString}. */ public PdfString getMappingName() { return getPdfObject().getAsString(PdfName.TM); } /** * Checks whether a certain flag, or any of a combination of flags, is set * for this form field. * * @param flag an int interpreted as a series of a binary flags. * @return true if any of the flags specified in the parameter is also set * in the form field. */ public boolean getFieldFlag(int flag) { return (getFieldFlags() & flag) != 0; } /** * Adds a flag, or combination of flags, for the form field. This method is * intended to be used one flag at a time, but this is not technically * enforced. To replace the current value, use * {@link #setFieldFlags(int)}. * * @param flag an int interpreted as a series of a binary flags. * @return the edited {@link PdfFormField}. */ public PdfFormField setFieldFlag(int flag) { return setFieldFlag(flag, true); } /** * Adds or removes a flag, or combination of flags, for the form field. This * method is intended to be used one flag at a time, but this is not * technically enforced. To replace the current value, use * {@link #setFieldFlags(int)}. * * @param flag an int interpreted as a series of a binary flags. * @param value if true, adds the flag(s). if false, * removes the flag(s). * @return the edited {@link PdfFormField}. */ public PdfFormField setFieldFlag(int flag, boolean value) { int flags = getFieldFlags(); if (value) { flags |= flag; } else { flags &= ~flag; } return setFieldFlags(flags); } /** * If true, the field can contain multiple lines of text; if false, the field's text is restricted to a single line. * * @return whether the field can span over multiple lines. */ public boolean isMultiline() { return getFieldFlag(FF_MULTILINE); } /** * If true, the field is intended for entering a secure password that should not be echoed visibly to the screen. * Characters typed from the keyboard should instead be echoed in some unreadable form, such as asterisks * or bullet characters. * * @return whether or not the contents of the field must be obfuscated. */ public boolean isPassword() { return getFieldFlag(FF_PASSWORD); } /** * Sets a flag, or combination of flags, for the form field. This method * replaces the previous value. Compare with {@link #setFieldFlag(int)} * which adds a flag to the existing flags. * * @param flags an int interpreted as a series of a binary flags. * @return the edited {@link PdfFormField}. */ public PdfFormField setFieldFlags(int flags) { int oldFlags = getFieldFlags(); put(PdfName.Ff, new PdfNumber(flags)); if (((oldFlags ^ flags) & PdfTextFormField.FF_COMB) != 0 && PdfName.Tx.equals(getFormType()) && PdfFormCreator.createTextFormField(getPdfObject()).getMaxLen() != 0) regenerateField(); return this; } /** * Gets the current list of PDF form field flags. * * @return the current list of flags, encoded as an int. */ public int getFieldFlags() { PdfNumber f = getPdfObject().getAsNumber(PdfName.Ff); if (f != null) { return f.intValue(); } else { PdfFormField parent = getParentField(); if (parent != null) { return parent.getFieldFlags(); } else { return 0; } } } /** * Gets the current value contained in the form field. * * @return the current value, as a {@link PdfObject}. */ public PdfObject getValue() { PdfObject value = getPdfObject().get(PdfName.V); // V is not taken into account if T is missing. This is the way Acrobat behaves. if ((getPdfObject().get(PdfName.T) == null || value == null) && getParentField() != null) { return getParentField().getValue(); } return value; } /** * Gets the current value contained in the form field. * * @return the current value, as a {@link String}. */ public String getValueAsString() { PdfObject value = getValue(); if (value == null) { return ""; } else if (value instanceof PdfStream) { return new String(((PdfStream) value).getBytes(), StandardCharsets.UTF_8); } else if (value instanceof PdfName) { return ((PdfName) value).getValue(); } else if (value instanceof PdfString) { return ((PdfString) value).toUnicodeString(); } else { return ""; } } /** * Gets the current display value of the form field. * * @return the current display value, as a {@link String}, if it exists. * If not, returns the value as a {@link String}. */ public String getDisplayValue() { if (displayValue != null) { return displayValue; } else if (text != null) { return text; } else { return getValueAsString(); } } /** * Sets the default fallback value for the form field. * * @param value the default value. * @return the edited {@link PdfFormField}. */ public PdfFormField setDefaultValue(PdfObject value) { put(PdfName.DV, value); return this; } /** * Gets the default fallback value for the form field. * * @return the default value. */ public PdfObject getDefaultValue() { return getPdfObject().get(PdfName.DV); } /** * Sets an additional action for the form field. * * @param key the dictionary key to use for storing the action. * @param action the action. * @return the edited {@link PdfFormField}. */ public PdfFormField setAdditionalAction(PdfName key, PdfAction action) { PdfAction.setAdditionalAction(this, key, action); return this; } /** * Gets the currently additional action dictionary for the form field. * * @return the additional action dictionary. */ public PdfDictionary getAdditionalAction() { return getPdfObject().getAsDictionary(PdfName.AA); } /** * Sets options for the form field. Only to be used for checkboxes and radio buttons. * * @param options an array of {@link PdfString} objects that each represent * the 'on' state of one of the choices. * @return the edited {@link PdfFormField}. */ public PdfFormField setOptions(PdfArray options) { put(PdfName.Opt, options); return this; } /** * Gets options for the form field. Should only return usable values for * checkboxes and radio buttons. * * @return the options, as an {@link PdfArray} of {@link PdfString} objects. */ public PdfArray getOptions() { return getPdfObject().getAsArray(PdfName.Opt); } /** * Gets all {@link PdfWidgetAnnotation} that its children refer to. * * @return a list of {@link PdfWidgetAnnotation}. */ public List getWidgets() { List widgets = new ArrayList<>(); for (AbstractPdfFormField child : childFields) { PdfDictionary kid = child.getPdfObject(); PdfName subType = kid.getAsName(PdfName.Subtype); if (subType != null && subType.equals(PdfName.Widget)) { widgets.add((PdfWidgetAnnotation) PdfAnnotation.makeAnnotation(kid)); } } return widgets; } /** * Gets all child form field's annotations {@link PdfFormAnnotation} of this form field. * * @return a list of {@link PdfFormAnnotation}. */ public List getChildFormAnnotations() { List annots = new ArrayList<>(); for (AbstractPdfFormField child : childFields) { if (child instanceof PdfFormAnnotation) { annots.add((PdfFormAnnotation)child); } } return annots; } /** * Gets a single child form field's annotation {@link PdfFormAnnotation}. * * @return {@link PdfFormAnnotation} or null if there are no child annotations. */ public PdfFormAnnotation getFirstFormAnnotation() { for (AbstractPdfFormField child : childFields) { if (child instanceof PdfFormAnnotation) { return (PdfFormAnnotation)child; } } return null; } /** * {@inheritDoc} * * @return {@inheritDoc} */ @Override public PdfString getDefaultAppearance() { PdfString defaultAppearance = getPdfObject().getAsString(PdfName.DA); if (defaultAppearance == null) { PdfDictionary parent = getParent(); if (parent != null) { //If this is not merged form field we should get default appearance from the parent which actually is a //form field dictionary if (parent.containsKey(PdfName.FT)) { defaultAppearance = parent.getAsString(PdfName.DA); } } } // DA is an inherited key, therefore AcroForm shall be checked if there is no parent or no DA in parent. if (defaultAppearance == null) { defaultAppearance = (PdfString) getAcroFormKey(PdfName.DA, PdfObject.STRING); } return defaultAppearance; } /** * Updates DA for Variable text, Push button and choice form fields. * The resources required for DA will be put to AcroForm's DR. * Note, for other form field types DA will be removed. */ public void updateDefaultAppearance() { if (hasDefaultAppearance()) { assert getFont() != null; PdfDictionary defaultResources = (PdfDictionary) getAcroFormObject(PdfName.DR, PdfObject.DICTIONARY); if (defaultResources == null) { // Ensure that AcroForm dictionary exists addAcroFormToCatalog(); defaultResources = new PdfDictionary(); putAcroFormObject(PdfName.DR, defaultResources); } PdfDictionary fontResources = defaultResources.getAsDictionary(PdfName.Font); if (fontResources == null) { fontResources = new PdfDictionary(); defaultResources.put(PdfName.Font, fontResources); } PdfName fontName = getFontNameFromDR(fontResources, getFont().getPdfObject()); if (fontName == null) { fontName = getUniqueFontNameForDR(fontResources); fontResources.put(fontName, getFont().getPdfObject()); fontResources.setModified(); } put(PdfName.DA, generateDefaultAppearance(fontName, getFontSize(), color)); // Font from DR may not be added to document through PdfResource. getDocument().addFont(getFont()); } else { getPdfObject().remove(PdfName.DA); setModified(); } } /** * Gets a code specifying the form of quadding (justification) to be used in displaying the text: * 0 Left-justified * 1 Centered * 2 Right-justified * * @return the current justification attribute. */ public TextAlignment getJustification() { Integer justification = getPdfObject().getAsInt(PdfName.Q); if (justification == null && getParent() != null) { justification = getParent().getAsInt(PdfName.Q); } return justification == null ? null : numberToHorizontalAlignment((int) justification); } /** * Sets a code specifying the form of quadding (justification) to be used in displaying the text: * 0 Left-justified * 1 Centered * 2 Right-justified * * @param justification the value to set the justification attribute to. * @return the edited {@link PdfFormField}. */ public PdfFormField setJustification(TextAlignment justification) { if (justification != null) { put(PdfName.Q, new PdfNumber(justification.ordinal())); regenerateField(); } return this; } /** * Gets a default style string, as described in "Rich Text Strings" section of Pdf spec. * * @return the default style, as a {@link PdfString}. */ public PdfString getDefaultStyle() { return getPdfObject().getAsString(PdfName.DS); } /** * Sets a default style string, as described in "Rich Text Strings" section of Pdf spec. * * @param defaultStyleString a new default style for the form field. * @return the edited {@link PdfFormField}. */ public PdfFormField setDefaultStyle(PdfString defaultStyleString) { put(PdfName.DS, defaultStyleString); return this; } /** * Gets a rich text string, as described in "Rich Text Strings" section of Pdf spec. * May be either {@link PdfStream} or {@link PdfString}. * * @return the current rich text value. */ public PdfObject getRichText() { return getPdfObject().get(PdfName.RV); } /** * Sets a rich text string, as described in "Rich Text Strings" section of Pdf spec. * May be either {@link PdfStream} or {@link PdfString}. * * @param richText a new rich text value. * @return the edited {@link PdfFormField}. */ public PdfFormField setRichText(PdfObject richText) { put(PdfName.RV, richText); return this; } /** * Changes the type of graphical marker used to mark a checkbox as 'on'. * Notice that in order to complete the change one should call * {@link #regenerateField() regenerateField} method. * * @param checkType the new checkbox marker. * @return the edited {@link PdfFormField}. */ public PdfFormField setCheckType(CheckBoxType checkType) { if (checkType == null) { checkType = CheckBoxType.CROSS; } this.checkType = new NullableContainer<>(checkType); if (getPdfAConformanceLevel() != null) { return this; } try { font = PdfFontFactory.createFont(StandardFonts.ZAPFDINGBATS); } catch (IOException e) { throw new PdfException(e); } return this; } /** * {@inheritDoc} * * @return {@inheritDoc} */ @Override public boolean regenerateField() { boolean result = true; if (isFieldRegenerationEnabled()) { updateDefaultAppearance(); } else { result = false; } for (AbstractPdfFormField child : childFields) { if (child instanceof PdfFormAnnotation) { PdfFormAnnotation annotation = (PdfFormAnnotation) child; result &= annotation.regenerateWidget(); } else { child.regenerateField(); } } return result; } /** * Sets the ReadOnly flag, specifying whether or not the field can be changed. * * @param readOnly if true, then the field cannot be changed. * @return the edited {@link PdfFormField}. */ public PdfFormField setReadOnly(boolean readOnly) { return setFieldFlag(FF_READ_ONLY, readOnly); } /** * Gets the ReadOnly flag, specifying whether or not the field can be changed. * * @return true if the field cannot be changed. */ public boolean isReadOnly() { return getFieldFlag(FF_READ_ONLY); } /** * Sets the Required flag, specifying whether or not the field must be filled in. * * @param required if true, then the field must be filled in. * @return the edited {@link PdfFormField}. */ public PdfFormField setRequired(boolean required) { return setFieldFlag(FF_REQUIRED, required); } /** * Gets the Required flag, specifying whether or not the field must be filled in. * * @return true if the field must be filled in. */ public boolean isRequired() { return getFieldFlag(FF_REQUIRED); } /** * Sets the NoExport flag, specifying whether or not exporting is forbidden. * * @param noExport if true, then exporting is forbidden * @return the edited {@link PdfFormField}. */ public PdfFormField setNoExport(boolean noExport) { return setFieldFlag(FF_NO_EXPORT, noExport); } /** * Gets the NoExport attribute. * * @return whether exporting the value following a form action is forbidden. */ public boolean isNoExport() { return getFieldFlag(FF_NO_EXPORT); } /** * Checks if the document that contains the field is created in reading mode. * * @return true if reading mode is used, false otherwise. */ public boolean isInReadingMode() { return getDocument().getWriter() == null; } /** * {@inheritDoc} * * @return {@inheritDoc} */ @Override public String[] getAppearanceStates() { Set names = new LinkedHashSet<>(); PdfString stringOpt = getPdfObject().getAsString(PdfName.Opt); if (stringOpt != null) { names.add(stringOpt.toUnicodeString()); } else { PdfArray arrayOpt = getPdfObject().getAsArray(PdfName.Opt); if (arrayOpt != null) { for (PdfObject pdfObject : arrayOpt) { PdfString valStr = null; if (pdfObject.isArray()) { valStr = ((PdfArray) pdfObject).getAsString(1); } else if (pdfObject.isString()) { valStr = (PdfString) pdfObject; } if (valStr != null) { names.add(valStr.toUnicodeString()); } } } } for (AbstractPdfFormField child : childFields) { String[] states = child.getAppearanceStates(); Collections.addAll(names, states); } return names.toArray(new String[names.size()]); } /** * {@inheritDoc} */ @Override public void release() { for (AbstractPdfFormField child : childFields) { child.release(); } childFields.clear(); childFields = null; super.release(); } /** * {@inheritDoc} * * @param color {@inheritDoc} * @return {@inheritDoc} */ @Override public AbstractPdfFormField setColor(Color color) { this.color = color; for (AbstractPdfFormField child : childFields) { child.setColorNoRegenerate(color); } regenerateField(); return this; } @Override void updateFontAndFontSize(PdfFont font, float fontSize) { super.updateFontAndFontSize(font, fontSize); for (AbstractPdfFormField child : childFields) { child.updateFontAndFontSize(font, fontSize); } } static String optionsArrayToString(PdfArray options) { StringBuilder sb = new StringBuilder(); for (PdfObject obj : options) { if (obj.isString()) { sb.append(((PdfString) obj).toUnicodeString()).append('\n'); } else if (obj.isArray()) { PdfObject element = ((PdfArray) obj).size() > 1 ? ((PdfArray) obj).get(1) : null; if (element != null && element.isString()) { sb.append(((PdfString) element).toUnicodeString()).append('\n'); } } else { sb.append('\n'); } } // last '\n' sb.deleteCharAt(sb.length() - 1); return sb.toString(); } /** * Adds a field to the children of the current field. * * @param kid the field, which should become a child. * @return the kid itself. */ AbstractPdfFormField setChildField(AbstractPdfFormField kid) { kid.setParent(this); this.childFields.add(kid); return kid; } /** * Replaces /Kids value with passed kids dictionaries, and keeps old flashed fields there. * Also updates childFields array for {@link PdfFormField}. * * @param kids collection of new kids. */ void replaceKids(Collection kids) { PdfArray kidsValues = new PdfArray(); // Field may already have flushed widgets in /Kids, so we need to keep them. PdfArray oldKids = getKids(); if (oldKids != null) { for (PdfObject kid : oldKids) { if (kid.isFlushed()) { kidsValues.add(kid); } } } // Update childFields and /Kids. this.childFields.clear(); for (AbstractPdfFormField kid : kids) { kid.setParent(this); kidsValues.add(kid.getPdfObject()); this.childFields.add(kid); } put(PdfName.Kids, kidsValues); } private static PdfString generateDefaultAppearance(PdfName font, float fontSize, Color textColor) { assert font != null; ByteArrayOutputStream output = new ByteArrayOutputStream(); PdfOutputStream pdfStream = new PdfOutputStream(new OutputStream<>(output)); final byte[] g = new byte[]{(byte) 'g'}; final byte[] rg = new byte[]{(byte) 'r', (byte) 'g'}; final byte[] k = new byte[]{(byte) 'k'}; final byte[] Tf = new byte[]{(byte) 'T', (byte) 'f'}; pdfStream.write(font) .writeSpace() .writeFloat(fontSize).writeSpace() .writeBytes(Tf); if (textColor != null) { if (textColor instanceof DeviceGray) { pdfStream.writeSpace() .writeFloats(textColor.getColorValue()) .writeSpace() .writeBytes(g); } else if (textColor instanceof DeviceRgb) { pdfStream.writeSpace() .writeFloats(textColor.getColorValue()) .writeSpace() .writeBytes(rg); } else if (textColor instanceof DeviceCmyk) { pdfStream.writeSpace() .writeFloats(textColor.getColorValue()) .writeSpace() .writeBytes(k); } else { LOGGER.error(FormsLogMessageConstants.UNSUPPORTED_COLOR_IN_DA); } } return new PdfString(output.toByteArray()); } private static PdfName getTypeFromParent(PdfDictionary field) { PdfDictionary parent = field.getAsDictionary(PdfName.Parent); PdfName formType = field.getAsName(PdfName.FT); if (parent != null) { formType = parent.getAsName(PdfName.FT); if (formType == null) { formType = getTypeFromParent(parent); } } return formType; } private static TextAlignment numberToHorizontalAlignment(int alignment) { switch (alignment) { case 1: return TextAlignment.CENTER; case 2: return TextAlignment.RIGHT; default: return TextAlignment.LEFT; } } private PdfFormField setFieldValue(String value, boolean generateAppearance) { if (value == null) { LOGGER.warn(FormsLogMessageConstants.FIELD_VALUE_CANNOT_BE_NULL); return this; } // First, get rid of displayValue displayValue = null; PdfName formType = getFormType(); if (PdfName.Btn.equals(formType)) { if (getFieldFlag(PdfButtonFormField.FF_PUSH_BUTTON)) { try { img = ImageDataFactory.create(Base64.decode(value)); } catch (Exception e) { if (generateAppearance) { // Display value. for (PdfFormAnnotation annot : getChildFormAnnotations()) { annot.setCaption(value, false); } } else { text = value; } } } else { // We expect that radio buttons should have only widget children, // so we need to get rid of the form fields kids PdfFormFieldMergeUtil.processDirtyAnnotations(this, true); put(PdfName.V, new PdfName(value)); if (generateAppearance && !getFieldFlag(PdfButtonFormField.FF_RADIO)) { if (tryGenerateCheckboxAppearance(value)) { return this; } } for (PdfWidgetAnnotation widget : getWidgets()) { List states = Arrays.asList(PdfFormAnnotation .makeFormAnnotation(widget.getPdfObject(), getDocument()).getAppearanceStates()); if (states.contains(value)) { widget.setAppearanceState(new PdfName(value)); } else { widget.setAppearanceState(new PdfName(PdfFormAnnotation.OFF_STATE_VALUE)); } } } } else { if (PdfName.Ch.equals(formType)) { if (this instanceof PdfChoiceFormField) { ((PdfChoiceFormField) this).setListSelected(new String[] {value}, false); } else { PdfChoiceFormField choice = PdfFormCreator.createChoiceFormField(this.getPdfObject()); choice.setListSelected(new String[] {value}, false); } } else { put(PdfName.V, new PdfString(value, PdfEncodings.UNICODE_BIG)); } } if (generateAppearance) { regenerateField(); } this.setModified(); return this; } /** * Distinguish mutually exclusive and regular checkboxes: check all the on states of the widgets, if they are * not all equal, then consider that this checkbox is mutually exclusive and do nothing, otherwise regenerate * normal appearance with value as on appearance state for all the widgets. * * @param value not empty value different from "Off". */ private boolean tryGenerateCheckboxAppearance(String value) { if (value == null || value.isEmpty() || PdfFormAnnotation.OFF_STATE_VALUE.equals(value)) { return false; } Set allStates = new HashSet<>(); for (PdfFormAnnotation annotation : getChildFormAnnotations()) { allStates.addAll(Arrays.asList(annotation.getAppearanceStates())); if (allStates.size() > 2) { return false; } } allStates.remove(PdfFormAnnotation.OFF_STATE_VALUE); if (allStates.isEmpty() || allStates.size() == 1 && !value.equals(allStates.toArray(new String[allStates.size()])[0])) { for (PdfFormAnnotation annotation : getChildFormAnnotations()) { annotation.setCheckBoxAppearanceOnStateName(value); } updateDefaultAppearance(); return true; } return false; } private boolean mergeKidsIfKidWithSuchNameExists(AbstractPdfFormField newKid, boolean throwExceptionOnError) { if (childFields.contains(newKid)) { return true; } if (isInReadingMode() || PdfFormAnnotationUtil.isPureWidget(newKid.getPdfObject())) { return false; } String newKidPartialName = PdfFormFieldMergeUtil.getPartialName(newKid); for (AbstractPdfFormField kid : childFields) { String kidPartialName = PdfFormFieldMergeUtil.getPartialName(kid); if (kidPartialName != null && kidPartialName.equals(newKidPartialName)) { // Merge kid with the first found field with the same name. return PdfFormFieldMergeUtil.mergeTwoFieldsWithTheSameNames((PdfFormField) kid, (PdfFormField) newKid, throwExceptionOnError); } } return false; } private boolean hasDefaultAppearance() { PdfName type = getFormType(); return type == PdfName.Tx || type == PdfName.Ch || (type == PdfName.Btn && (getFieldFlags() & PdfButtonFormField.FF_PUSH_BUTTON) != 0); } private PdfName getUniqueFontNameForDR(PdfDictionary fontResources) { int indexer = 1; Set fontNames = fontResources.keySet(); PdfName uniqueName; do { uniqueName = new PdfName("F" + indexer++); } while (fontNames.contains(uniqueName)); return uniqueName; } private PdfName getFontNameFromDR(PdfDictionary fontResources, PdfObject font) { for (Map.Entry drFont : fontResources.entrySet()) { if (drFont.getValue() == font) { return drFont.getKey(); } } return null; } /** * Puts object directly to AcroForm dictionary. * It works much faster than consequent invocation of {@link PdfAcroForm#getAcroForm(PdfDocument, boolean)} * and {@link PdfAcroForm#getPdfObject()}. *

* Note, this method assume that Catalog already has AcroForm object. * {@link #addAcroFormToCatalog()} should be called explicitly. * * @param acroFormKey the key of the object. * @param acroFormObject the object to add. */ private void putAcroFormObject(PdfName acroFormKey, PdfObject acroFormObject) { getDocument().getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm).put(acroFormKey, acroFormObject); } private void addAcroFormToCatalog() { if (getDocument().getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm) == null) { PdfDictionary acroform = new PdfDictionary(); acroform.makeIndirect(getDocument()); // PdfName.Fields is the only required key. acroform.put(PdfName.Fields, new PdfArray()); getDocument().getCatalog().put(PdfName.AcroForm, acroform); } } private PdfObject getAcroFormKey(PdfName key, int type) { PdfObject acroFormKey = null; PdfDocument document = getDocument(); if (document != null) { PdfDictionary acroFormDictionary = document.getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm); if (acroFormDictionary != null) { acroFormKey = acroFormDictionary.get(key); } } return (acroFormKey != null && acroFormKey.getType() == type) ? acroFormKey : null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy