com.itextpdf.forms.fields.PdfFormAnnotation 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.fields;
import com.itextpdf.commons.datastructures.NullableContainer;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.fields.borders.FormBorderFactory;
import com.itextpdf.forms.fields.properties.CheckBoxType;
import com.itextpdf.forms.fields.properties.SignedAppearanceText;
import com.itextpdf.forms.form.FormProperty;
import com.itextpdf.forms.form.element.Button;
import com.itextpdf.forms.form.element.CheckBox;
import com.itextpdf.forms.form.element.ComboBoxField;
import com.itextpdf.forms.form.element.IFormField;
import com.itextpdf.forms.form.element.InputField;
import com.itextpdf.forms.form.element.ListBoxField;
import com.itextpdf.forms.form.element.Radio;
import com.itextpdf.forms.form.element.SelectFieldItem;
import com.itextpdf.forms.form.element.SignatureFieldAppearance;
import com.itextpdf.forms.form.element.TextArea;
import com.itextpdf.forms.form.renderer.checkboximpl.PdfCheckBoxRenderingStrategy;
import com.itextpdf.forms.logs.FormsLogMessageConstants;
import com.itextpdf.forms.util.FontSizeUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
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.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDate;
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.PdfPage;
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.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.borders.SolidBorder;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.properties.Background;
import com.itextpdf.layout.properties.BoxSizingPropertyValue;
import com.itextpdf.layout.properties.OverflowPropertyValue;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.properties.TransparentColor;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.renderer.MetaInfoContainer;
import java.util.LinkedHashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents a single annotation in form fields hierarchy 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 PdfFormAnnotation extends AbstractPdfFormField {
public static final int HIDDEN = 1;
public static final int VISIBLE_BUT_DOES_NOT_PRINT = 2;
public static final int HIDDEN_BUT_PRINTABLE = 3;
public static final int VISIBLE = 4;
/**
* Value which represents "off" state of form field.
*/
public static final String OFF_STATE_VALUE = "Off";
/**
* Value which represents "on" state of form field.
*/
public static final String ON_STATE_VALUE = "Yes";
private static final Logger LOGGER = LoggerFactory.getLogger(PdfFormAnnotation.class);
private static final String LINE_ENDINGS_REGEXP = "\\r\\n|\\r|\\n";
private static final float EPS = 1e-4f;
protected float borderWidth = 1;
protected Color backgroundColor;
protected Color borderColor;
private IFormField formFieldElement;
/**
* Creates a form field annotation as a wrapper of a {@link PdfWidgetAnnotation}.
*
* @param widget The widget which will be a kid of the {@link PdfFormField}
* @param pdfDocument The {@link PdfDocument} instance.
*/
protected PdfFormAnnotation(PdfWidgetAnnotation widget, PdfDocument pdfDocument) {
this(widget.makeIndirect(pdfDocument).getPdfObject());
}
/**
* Creates a form field annotation 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.
*/
protected PdfFormAnnotation(PdfDictionary pdfObject) {
super(pdfObject);
}
/**
* Creates a {@link PdfFormAnnotation} object.
*
* @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 PdfFormAnnotation}, or null
if
* pdfObject
is not a widget annotation.
*/
public static PdfFormAnnotation makeFormAnnotation(PdfObject pdfObject, PdfDocument document) {
if (!pdfObject.isDictionary()) {
return null;
}
PdfFormAnnotation field;
PdfDictionary dictionary = (PdfDictionary) pdfObject;
final PdfName subType = dictionary.getAsName(PdfName.Subtype);
// If widget annotation
if (PdfName.Widget.equals(subType)) {
field = PdfFormCreator.createFormAnnotation((PdfWidgetAnnotation) PdfAnnotation.makeAnnotation(dictionary),
document);
} else {
return null;
}
field.makeIndirect(document);
if (document != null && document.getReader() != null
&& document.getReader().getPdfAConformanceLevel() != null) {
field.pdfConformanceLevel = document.getReader().getPdfAConformanceLevel();
}
return field;
}
/**
* Gets {@link PdfWidgetAnnotation} that this form field refers to.
*
* @return {@link PdfWidgetAnnotation}.
*/
public PdfWidgetAnnotation getWidget() {
PdfName subType = getPdfObject().getAsName(PdfName.Subtype);
if (subType != null && subType.equals(PdfName.Widget)) {
return (PdfWidgetAnnotation) PdfAnnotation.makeAnnotation(getPdfObject());
}
// Should never be here
assert "You are not an annotation then" == null;
return null;
}
/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
public PdfString getDefaultAppearance() {
return getPdfObject().getAsString(PdfName.DA);
}
/**
* {@inheritDoc}
*
* @return {@inheritDoc}
*/
@Override
public boolean regenerateField() {
if (parent != null && parent.isFieldRegenerationEnabled()) {
parent.updateDefaultAppearance();
}
return regenerateWidget();
}
/**
* Gets the appearance state names.
*
* @return an array of Strings containing the names of the appearance states.
*/
@Override
public String[] getAppearanceStates() {
Set names = new LinkedHashSet<>();
PdfDictionary dic = getPdfObject();
dic = dic.getAsDictionary(PdfName.AP);
if (dic != null) {
dic = dic.getAsDictionary(PdfName.N);
if (dic != null) {
for (PdfName state : dic.keySet()) {
names.add(state.getValue());
}
}
}
return names.toArray(new String[names.size()]);
}
@Override
void retrieveStyles() {
super.retrieveStyles();
PdfDictionary appearanceCharacteristics = getPdfObject().getAsDictionary(PdfName.MK);
if (appearanceCharacteristics != null) {
backgroundColor = appearancePropToColor(appearanceCharacteristics, PdfName.BG);
Color extractedBorderColor = appearancePropToColor(appearanceCharacteristics, PdfName.BC);
if (extractedBorderColor != null) {
borderColor = extractedBorderColor;
}
if (parent != null) {
parent.text = appearancePropToCaption(appearanceCharacteristics);
}
}
}
/**
* Basic setter for the backgroundColor
property. Regenerates
* the field appearance after setting the new value.
*
* @param backgroundColor The new color to be set or {@code null} if no background needed.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
PdfDictionary mk;
PdfWidgetAnnotation kid = getWidget();
mk = kid.getAppearanceCharacteristics();
if (mk == null) {
mk = new PdfDictionary();
}
if (backgroundColor == null) {
mk.remove(PdfName.BG);
} else {
mk.put(PdfName.BG, new PdfArray(backgroundColor.getColorValue()));
}
kid.setAppearanceCharacteristics(mk);
regenerateField();
return this;
}
/**
* Basic setter for the push button caption. Regenerates the field appearance after setting the new caption.
*
* @param caption button caption to be set.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setCaption(String caption) {
return setCaption(caption, true);
}
/**
* Basic setter for the push button caption. Regenerates the field appearance after setting the new caption
* if corresponding parameter is specified.
*
* @param caption button caption to be set.
* @param regenerateField true if field should be regenerated, false otherwise.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setCaption(String caption, boolean regenerateField) {
if (parent != null) {
parent.text = caption;
}
PdfDictionary mk;
PdfWidgetAnnotation kid = getWidget();
mk = kid.getAppearanceCharacteristics();
if (mk == null) {
mk = new PdfDictionary();
}
if (caption == null) {
mk.remove(PdfName.CA);
} else {
mk.put(PdfName.CA, new PdfString(caption));
}
kid.setAppearanceCharacteristics(mk);
if (regenerateField) {
regenerateField();
}
return this;
}
/**
* Get rotation property specified in this form annotation.
*
* @return {@code int} value which represents field's rotation
*/
public int getRotation() {
PdfDictionary mk = getWidget().getAppearanceCharacteristics();
return mk == null || mk.getAsInt(PdfName.R) == null ? 0 : (int) mk.getAsInt(PdfName.R);
}
/**
* Basic setter for the degRotation
property. Regenerates
* the field appearance after setting the new value.
*
* @param degRotation The new degRotation to be set
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setRotation(int degRotation) {
if (degRotation % 90 != 0) {
throw new IllegalArgumentException("degRotation.must.be.a.multiple.of.90");
} else {
degRotation %= 360;
if (degRotation < 0) {
degRotation += 360;
}
}
PdfDictionary mk = getWidget().getAppearanceCharacteristics();
if (mk == null) {
mk = new PdfDictionary();
this.put(PdfName.MK, mk);
}
mk.put(PdfName.R, new PdfNumber(degRotation));
regenerateField();
return this;
}
/**
* Sets the action on {@link PdfWidgetAnnotation widget} of this annotation form field.
*
* @param action The action.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setAction(PdfAction action) {
PdfWidgetAnnotation widget = getWidget();
if (widget != null) {
widget.setAction(action);
}
return this;
}
/**
* Set the visibility flags of the form field annotation.
* Options are: HIDDEN, HIDDEN_BUT_PRINTABLE, VISIBLE, VISIBLE_BUT_DOES_NOT_PRINT.
*
* @param visibility visibility option.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setVisibility(int visibility) {
switch (visibility) {
case HIDDEN:
put(PdfName.F, new PdfNumber(PdfAnnotation.PRINT | PdfAnnotation.HIDDEN));
break;
case VISIBLE_BUT_DOES_NOT_PRINT:
break;
case HIDDEN_BUT_PRINTABLE:
put(PdfName.F, new PdfNumber(PdfAnnotation.PRINT | PdfAnnotation.NO_VIEW));
break;
case VISIBLE:
default:
put(PdfName.F, new PdfNumber(PdfAnnotation.PRINT));
break;
}
return this;
}
/**
* Gets the border width for the field.
*
* @return the current border width.
*/
public float getBorderWidth() {
PdfDictionary bs = getWidget().getBorderStyle();
if (bs != null) {
PdfNumber w = bs.getAsNumber(PdfName.W);
if (w != null) {
borderWidth = w.floatValue();
}
}
return borderWidth;
}
/**
* Sets the border width for the field.
*
* @param borderWidth The new border width.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setBorderWidth(float borderWidth) {
// Acrobat doesn't support float border width therefore we round it.
int roundedBorderWidth = (int) Math.round(borderWidth);
PdfDictionary bs = getWidget().getBorderStyle();
if (bs == null) {
bs = new PdfDictionary();
put(PdfName.BS, bs);
}
bs.put(PdfName.W, new PdfNumber(roundedBorderWidth));
this.borderWidth = roundedBorderWidth;
regenerateField();
return this;
}
/**
* Get border object specified in the widget annotation dictionary.
*
* @return {@link Border} specified in the widget annotation dictionary
*/
public Border getBorder() {
float borderWidth = getBorderWidth();
Border border = FormBorderFactory.getBorder(
this.getWidget().getBorderStyle(), borderWidth, borderColor, backgroundColor);
if (border == null && borderWidth > 0 && borderColor != null) {
border = new SolidBorder(borderColor, Math.max(1, borderWidth));
}
return border;
}
/**
* Sets the border style for the field.
*
* @param style the new border style.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setBorderStyle(PdfDictionary style) {
getWidget().setBorderStyle(style);
regenerateField();
return this;
}
/**
* Sets the Border Color.
*
* @param color the new value for the Border Color.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setBorderColor(Color color) {
borderColor = color;
PdfDictionary mk;
PdfWidgetAnnotation kid = getWidget();
mk = kid.getAppearanceCharacteristics();
if (mk == null) {
mk = new PdfDictionary();
}
if (borderColor == null) {
mk.remove(PdfName.BC);
} else {
mk.put(PdfName.BC, new PdfArray(borderColor.getColorValue()));
}
kid.setAppearanceCharacteristics(mk);
regenerateField();
return this;
}
/**
* Specifies on which page the form field's widget must be shown.
*
* @param pageNum the page number.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setPage(int pageNum) {
PdfWidgetAnnotation widget = getWidget();
if (widget != null) {
widget.setPage(getDocument().getPage(pageNum));
}
return this;
}
/**
* This method sets the model element associated with the current annotation and can be useful to take into account
* when drawing those properties that the annotation does not have. Note that annotation properties will take
* precedence, so such properties cannot be overridden by using this method (e.g. background, text color, etc.).
*
*
* Also note that the model element won't be used for annotations for choice form field.
*
* @param element model element to set.
*
* @return this {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setFormFieldElement(IFormField element) {
this.formFieldElement = element;
regenerateWidget();
return this;
}
/**
* Sets an appearance for (the widgets related to) the form field.
*
* @param appearanceType the type of appearance stream to be added
*
* - PdfName.N: normal appearance
*
- PdfName.R: rollover appearance
*
- PdfName.D: down appearance
*
* @param appearanceState the state of the form field that needs to be true
* for the appearance to be used. Differentiates between several streams
* of the same type.
* @param appearanceStream the appearance instructions, as a {@link PdfStream}.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setAppearance(PdfName appearanceType, String appearanceState,
PdfStream appearanceStream) {
PdfDictionary dic = getPdfObject();
PdfDictionary ap = dic.getAsDictionary(PdfName.AP);
if (ap != null) {
PdfDictionary appearanceDictionary = ap.getAsDictionary(appearanceType);
if (appearanceDictionary == null) {
ap.put(appearanceType, appearanceStream);
} else {
appearanceDictionary.put(new PdfName(appearanceState), appearanceStream);
}
}
return this;
}
/**
* Sets on state name for the checkbox annotation normal appearance and regenerates widget.
*
* @param onStateName the new appearance name representing on state.
*
* @return The edited {@link PdfFormAnnotation}.
*/
public PdfFormAnnotation setCheckBoxAppearanceOnStateName(String onStateName) {
if (isCheckBox() && onStateName != null && !onStateName.isEmpty()
&& !PdfFormAnnotation.OFF_STATE_VALUE.equals(onStateName)) {
drawCheckBoxAndSaveAppearance(onStateName);
getWidget().setAppearanceState(new PdfName(onStateName.equals(parent.getValueAsString()) ?
onStateName : OFF_STATE_VALUE));
}
return this;
}
/**
* Gets a {@link Rectangle} that matches the current size and position of this form field.
*
* @param field current form field.
*
* @return a {@link Rectangle} that matches the current size and position of this form field
* annotation.
*/
protected Rectangle getRect(PdfDictionary field) {
PdfArray rect = field.getAsArray(PdfName.Rect);
return rect == null ? null : rect.toRectangle();
}
/**
* Draws a border using the borderWidth and borderColor of the form field.
*
* @param canvas The {@link PdfCanvas} on which to draw
* @param xObject The PdfFormXObject
* @param width The width of the rectangle to draw
* @param height The height of the rectangle to draw
*/
protected void drawBorder(PdfCanvas canvas, PdfFormXObject xObject, float width, float height) {
canvas.saveState();
float borderWidth = getBorderWidth();
PdfDictionary bs = getWidget().getBorderStyle();
if (borderWidth < 0) {
borderWidth = 0;
}
if (backgroundColor != null) {
canvas
.setFillColor(backgroundColor)
.rectangle(0, 0, width, height)
.fill();
}
if (borderWidth > 0 && borderColor != null) {
borderWidth = Math.max(1, borderWidth);
canvas
.setStrokeColor(borderColor)
.setLineWidth(borderWidth);
Border border = FormBorderFactory.getBorder(bs, borderWidth, borderColor, backgroundColor);
if (border != null) {
float borderWidthX2 = borderWidth + borderWidth;
border.draw(canvas, new Rectangle(borderWidth, borderWidth,
width - borderWidthX2, height - borderWidthX2));
} else {
canvas
.rectangle(0, 0, width, height)
.stroke();
}
}
PdfArray matrix = getRotationMatrix(getRotation(), height, width);
if (matrix != null) {
xObject.put(PdfName.Matrix, matrix);
}
canvas.restoreState();
}
/**
* Draws the appearance of a push button and saves it into an appearance stream.
*/
protected void drawPushButtonFieldAndSaveAppearance() {
Rectangle rectangle = getRect(this.getPdfObject());
if (rectangle == null) {
return;
}
final int fieldRotation = getRotation();
PdfArray matrix = getRotationMatrix(fieldRotation, rectangle.getHeight(), rectangle.getWidth());
rectangle = applyRotation(fieldRotation, rectangle);
float width = rectangle.getWidth();
float height = rectangle.getHeight();
createInputButton();
setModelElementProperties(rectangle);
PdfFormXObject xObject = new PdfFormXObject(new Rectangle(0, 0, width, height));
if (matrix != null) {
xObject.put(PdfName.Matrix, matrix);
}
Canvas canvas = new Canvas(xObject, this.getDocument());
setMetaInfoToCanvas(canvas);
String caption = parent.getDisplayValue();
if (caption != null && !caption.isEmpty()) {
((Button) formFieldElement).setSingleLineValue(caption);
}
float imagePadding = borderColor == null ? 0 : borderWidth;
if (parent.img != null) {
// If we got here, the button will only contain the image that the user has set into the annotation.
// There is no way to pass other elements with this image.
formFieldElement.getChildren().clear();
Image image = new Image(new PdfImageXObject(parent.img), imagePadding, imagePadding);
image.setHeight(height - 2 * imagePadding);
image.setWidth(width - 2 * imagePadding);
((Button) formFieldElement).add(image);
} else if (parent.form != null) {
// If we got here, the button will only contain the image that the user has set as form into the annotation.
// There is no way to pass other elements with this image as form.
formFieldElement.getChildren().clear();
Image image = new Image(parent.form, imagePadding, imagePadding);
image.setHeight(height - 2 * imagePadding);
((Button) formFieldElement).add(image);
} else {
xObject.getResources().addFont(getDocument(), getFont());
}
canvas.add(formFieldElement);
PdfDictionary ap = new PdfDictionary();
PdfStream normalAppearanceStream = xObject.getPdfObject();
if (normalAppearanceStream != null) {
PdfName stateName = getPdfObject().getAsName(PdfName.AS);
if (stateName == null) {
stateName = new PdfName("push");
}
getPdfObject().put(PdfName.AS, stateName);
PdfDictionary normalAppearance = new PdfDictionary();
normalAppearance.put(stateName, normalAppearanceStream);
ap.put(PdfName.N, normalAppearance);
ap.setModified();
}
put(PdfName.AP, ap);
// We need to draw waitingDrawingElements (drawn inside close method), but the close method
// flushes TagTreePointer that will be used later, so set null to the corresponding property.
canvas.setProperty(Property.TAGGING_HELPER, null);
canvas.close();
formFieldElement.setInteractive(true);
}
/**
* Draws the appearance of a signature field and saves it into an appearance stream.
*/
protected void drawSignatureFormFieldAndSaveAppearance() {
Rectangle rectangle = getRect(this.getPdfObject());
if (PdfFormAnnotation.isFieldInvisible(rectangle)) {
// According to the spec, appearance stream is not required but can be an empty xObject
// if the width or height of the rectangle is 0.
final PdfDictionary appearanceDictionary = new PdfDictionary();
final PdfFormXObject normalAppearance = new PdfFormXObject(new Rectangle(0, 0));
normalAppearance.makeIndirect(this.getDocument());
appearanceDictionary.put(PdfName.N, normalAppearance.getPdfObject());
appearanceDictionary.setModified();
put(PdfName.AP, appearanceDictionary);
return;
}
// Rotation
PdfPage page = getWidget().getPage();
final int pageRotation = page == null ? 0 : page.getRotation();
final int additionalFieldRotation =
((PdfSignatureFormField) parent).isPageRotationIgnored() ? 0 : -pageRotation;
final int fieldRotation = getRotation() + additionalFieldRotation;
PdfArray matrix = getRotationMatrix(fieldRotation, rectangle.getHeight(), rectangle.getWidth());
rectangle = applyRotation(fieldRotation + pageRotation, rectangle);
createSigField();
setModelElementProperties(rectangle);
PdfFormXObject normalAppearance = new PdfFormXObject(
new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight()));
PdfCanvas normalAppearanceCanvas = new PdfCanvas(normalAppearance, getDocument());
if (matrix != null) {
normalAppearance.put(PdfName.Matrix, matrix);
}
PdfFormXObject topLayerXObject = createTopLayer(rectangle.getWidth(), rectangle.getHeight());
normalAppearance.getResources().addForm(topLayerXObject, new PdfName("FRM"));
normalAppearanceCanvas.addXObjectAt(topLayerXObject,
topLayerXObject.getBBox().getAsNumber(0).floatValue(),
topLayerXObject.getBBox().getAsNumber(1).floatValue());
PdfDictionary appearanceDict = new PdfDictionary();
PdfStream normalAppearanceStream = normalAppearance.getPdfObject();
appearanceDict.put(PdfName.N, normalAppearanceStream);
appearanceDict.setModified();
put(PdfName.AP, appearanceDict);
}
/**
* Draws the appearance of a radio button with a specified value and saves it into an appearance stream.
*
* @param value the value of the radio button.
*/
protected void drawRadioButtonAndSaveAppearance(String value) {
Rectangle rectangle = getRect(this.getPdfObject());
if (rectangle == null) {
return;
}
if (!(formFieldElement instanceof Radio)) {
// Create it one time and re-set properties during each widget regeneration.
formFieldElement = new Radio("");
}
final boolean wasChecked = Boolean.TRUE.equals(
formFieldElement.getProperty(FormProperty.FORM_FIELD_CHECKED));
setModelElementProperties(getRect(getPdfObject()));
// First draw off appearance
((Radio) formFieldElement).setChecked(false);
PdfFormXObject xObjectOff = new PdfFormXObject(
new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight()));
Canvas canvasOff = new Canvas(xObjectOff, this.getDocument());
setMetaInfoToCanvas(canvasOff);
canvasOff.add(formFieldElement);
PdfDictionary normalAppearance = new PdfDictionary();
normalAppearance.put(new PdfName(OFF_STATE_VALUE), xObjectOff.getPdfObject());
// Draw on appearance
if (value != null && !value.isEmpty() && !PdfFormAnnotation.OFF_STATE_VALUE.equals(value)) {
((Radio) formFieldElement).setChecked(true);
PdfFormXObject xObject = new PdfFormXObject(
new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight()));
Canvas canvas = new Canvas(xObject, this.getDocument());
setMetaInfoToCanvas(canvas);
canvas.add(formFieldElement);
normalAppearance.put(new PdfName(value), xObject.getPdfObject());
}
((Radio) formFieldElement).setChecked(wasChecked);
getWidget().setNormalAppearance(normalAppearance);
formFieldElement.setInteractive(true);
}
/**
* Draws the appearance of a list box form field and saves it into an appearance stream.
*/
protected void drawListFormFieldAndSaveAppearance() {
Rectangle rectangle = getRect(this.getPdfObject());
if (rectangle == null) {
return;
}
if (!(formFieldElement instanceof ListBoxField)) {
// Create it once and reset properties during each widget regeneration.
formFieldElement = new ListBoxField("", 0, parent.getFieldFlag(PdfChoiceFormField.FF_MULTI_SELECT));
}
formFieldElement.setProperty(FormProperty.FORM_FIELD_MULTIPLE,
parent.getFieldFlag(PdfChoiceFormField.FF_MULTI_SELECT));
PdfArray indices = getParent().getAsArray(PdfName.I);
PdfArray options = parent.getOptions();
for (int index = 0; index < options.size(); ++index) {
final PdfObject option = options.get(index);
String exportValue = null;
String displayValue = null;
if (option.isString()) {
exportValue = option.toString();
} else if (option.isArray()) {
PdfArray optionArray = (PdfArray) option;
if (optionArray.size() > 1) {
exportValue = optionArray.get(0).toString();
displayValue = optionArray.get(1).toString();
}
}
if (exportValue == null) {
continue;
}
final boolean selected = indices != null && indices.contains(new PdfNumber(index));
SelectFieldItem existingItem = ((ListBoxField) formFieldElement).getOption(exportValue);
if (existingItem == null) {
existingItem = new SelectFieldItem(exportValue, displayValue);
((ListBoxField) formFieldElement).addOption(existingItem);
}
existingItem.getElement().setProperty(Property.TEXT_ALIGNMENT, parent.getJustification());
existingItem.getElement().setProperty(Property.OVERFLOW_Y, OverflowPropertyValue.VISIBLE);
existingItem.getElement().setProperty(Property.OVERFLOW_X, OverflowPropertyValue.VISIBLE);
existingItem.getElement().setProperty(FormProperty.FORM_FIELD_SELECTED, selected);
}
formFieldElement.setProperty(Property.FONT, getFont());
if (getColor() != null) {
formFieldElement.setProperty(Property.FONT_COLOR, new TransparentColor(getColor()));
}
setModelElementProperties(rectangle);
PdfFormXObject xObject = new PdfFormXObject(
new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight()));
Canvas canvas = new Canvas(xObject, this.getDocument());
setMetaInfoToCanvas(canvas);
canvas.setProperty(Property.APPEARANCE_STREAM_LAYOUT, Boolean.TRUE);
canvas.getPdfCanvas().beginVariableText().saveState().endPath();
canvas.add(formFieldElement);
canvas.getPdfCanvas().restoreState().endVariableText();
getWidget().setNormalAppearance(xObject.getPdfObject());
formFieldElement.setInteractive(true);
}
/**
* Draws the appearance of a text form field and saves it into an appearance stream.
*/
protected void drawTextFormFieldAndSaveAppearance() {
Rectangle rectangle = getRect(this.getPdfObject());
if (rectangle == null) {
return;
}
String value = parent.getDisplayValue();
if (!(parent.isMultiline() && formFieldElement instanceof TextArea ||
!parent.isMultiline() && formFieldElement instanceof InputField)) {
// Create it one time and re-set properties during each widget regeneration.
formFieldElement = parent.isMultiline() ?
(IFormField) new TextArea("") :
(IFormField) new InputField("");
}
if (parent.isMultiline()) {
formFieldElement.setProperty(Property.FONT_SIZE, UnitValue.createPointValue(getFontSize()));
} else {
formFieldElement.setProperty(Property.FONT_SIZE,
UnitValue.createPointValue(getFontSize(new PdfArray(rectangle), parent.getValueAsString())));
value = value.replaceAll(LINE_ENDINGS_REGEXP, " ");
}
formFieldElement.setValue(value);
formFieldElement.setProperty(Property.FONT, getFont());
formFieldElement.setProperty(Property.TEXT_ALIGNMENT, parent.getJustification());
formFieldElement.setProperty(FormProperty.FORM_FIELD_PASSWORD_FLAG, getParentField().isPassword());
formFieldElement.setProperty(Property.ADD_MARKED_CONTENT_TEXT, Boolean.TRUE);
if (getColor() != null) {
formFieldElement.setProperty(Property.FONT_COLOR, new TransparentColor(getColor()));
}
// Rotation
final int fieldRotation = getRotation();
PdfArray matrix = getRotationMatrix(fieldRotation, rectangle.getHeight(), rectangle.getWidth());
rectangle = applyRotation(fieldRotation, rectangle);
setModelElementProperties(rectangle);
PdfFormXObject xObject = new PdfFormXObject(
new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight()));
if (matrix != null) {
xObject.put(PdfName.Matrix, matrix);
}
Canvas canvas = new Canvas(xObject, this.getDocument());
setMetaInfoToCanvas(canvas);
canvas.setProperty(Property.APPEARANCE_STREAM_LAYOUT, Boolean.TRUE);
canvas.add(formFieldElement);
formFieldElement.setInteractive(true);
getWidget().setNormalAppearance(xObject.getPdfObject());
}
/**
* Draws the appearance of a Combo box form field and saves it into an appearance stream.
*/
protected void drawComboBoxAndSaveAppearance() {
Rectangle rectangle = getRect(this.getPdfObject());
if (rectangle == null) {
return;
}
if (!(formFieldElement instanceof ComboBoxField)) {
formFieldElement = new ComboBoxField("");
}
ComboBoxField comboBoxField = (ComboBoxField) formFieldElement;
prepareComboBoxFieldWithCorrectOptionsAndValues(comboBoxField);
comboBoxField.setFont(getFont());
setModelElementProperties(rectangle);
if (getFontSize() <= 0) {
Rectangle r2 = rectangle.clone();
// because the border is drawn inside the rectangle, we need to take this into account
float marginToApply = borderWidth;
r2.applyMargins(marginToApply, marginToApply, marginToApply, marginToApply, false);
UnitValue estimatedFontSize = UnitValue.createPointValue(
getFontSize(new PdfArray(r2), parent.getValueAsString()));
comboBoxField.setFontSize(estimatedFontSize.getValue());
} else {
comboBoxField.setFontSize(getFontSize());
}
if (getColor() != null) {
comboBoxField.setFontColor(getColor());
}
comboBoxField.setTextAlignment(parent.getJustification());
Rectangle pdfXobjectRectangle = new Rectangle(0, 0, rectangle.getWidth(), rectangle.getHeight());
final PdfFormXObject xObject = new PdfFormXObject(pdfXobjectRectangle);
final Canvas canvas = new Canvas(xObject, getDocument());
canvas.setProperty(Property.APPEARANCE_STREAM_LAYOUT, Boolean.TRUE);
setMetaInfoToCanvas(canvas);
canvas.setFont(getFont());
canvas.getPdfCanvas().beginVariableText().saveState().endPath();
canvas.add(comboBoxField);
canvas.getPdfCanvas().restoreState().endVariableText();
getWidget().setNormalAppearance(xObject.getPdfObject());
formFieldElement.setInteractive(true);
}
private void prepareComboBoxFieldWithCorrectOptionsAndValues(ComboBoxField comboBoxField) {
for (PdfObject option : parent.getOptions()) {
SelectFieldItem item = null;
if (option.isString()) {
item = new SelectFieldItem(((PdfString) option).getValue());
}
if (option.isArray()) {
assert option instanceof PdfArray;
PdfArray array = (PdfArray) option;
final int thereShouldBeTwoElementsInArray = 2;
if (array.size() == thereShouldBeTwoElementsInArray) {
String exportValue = ((PdfString) array.get(0)).getValue();
String displayValue = ((PdfString) array.get(1)).getValue();
item = new SelectFieldItem(exportValue, displayValue);
}
}
if (item != null && comboBoxField.getOption(item.getExportValue()) == null) {
comboBoxField.addOption(item);
}
}
comboBoxField.setSelected(parent.getDisplayValue());
}
/**
* Draw a checkbox and save its appearance.
*
* @param onStateName the name of the appearance state for the checked state
*/
protected void drawCheckBoxAndSaveAppearance(String onStateName) {
final Rectangle rect = getRect(this.getPdfObject());
if (rect == null) {
return;
}
reconstructCheckBoxType();
createCheckBox();
final boolean wasChecked = Boolean.TRUE.equals(
formFieldElement.getProperty(FormProperty.FORM_FIELD_CHECKED));
final PdfDictionary normalAppearance = new PdfDictionary();
((CheckBox) formFieldElement).setChecked(false);
final PdfFormXObject xObjectOff = new PdfFormXObject(
new Rectangle(0, 0, rect.getWidth(), rect.getHeight()));
final Canvas canvasOff = new Canvas(xObjectOff, getDocument());
setMetaInfoToCanvas(canvasOff);
canvasOff.add(formFieldElement);
if (getPdfConformanceLevel() == null) {
xObjectOff.getResources().addFont(getDocument(), getFont());
}
normalAppearance.put(new PdfName(OFF_STATE_VALUE), xObjectOff.getPdfObject());
String onStateNameForAp = onStateName;
if (onStateName == null || onStateName.isEmpty() || PdfFormAnnotation.OFF_STATE_VALUE.equals(onStateName)) {
onStateNameForAp = ON_STATE_VALUE;
}
((CheckBox) formFieldElement).setChecked(true);
final PdfFormXObject xObject = new PdfFormXObject(
new Rectangle(0, 0, rect.getWidth(), rect.getHeight()));
final Canvas canvas = new Canvas(xObject, this.getDocument());
setMetaInfoToCanvas(canvas);
canvas.add(formFieldElement);
normalAppearance.put(new PdfName(onStateNameForAp), xObject.getPdfObject());
getWidget().setNormalAppearance(normalAppearance);
final PdfDictionary mk = new PdfDictionary();
// We put the zapfDingbats code of the checkbox in the MK dictionary to make sure there is a way
// to retrieve the checkbox type even if the appearance is not present.
mk.put(PdfName.CA,
new PdfString(PdfCheckBoxRenderingStrategy.ZAPFDINGBATS_CHECKBOX_MAPPING.getByKey(
parent.checkType.getValue())));
getWidget().put(PdfName.MK, mk);
formFieldElement.setInteractive(true);
((CheckBox) formFieldElement).setChecked(wasChecked);
}
static void setMetaInfoToCanvas(Canvas canvas) {
MetaInfoContainer metaInfo = FormsMetaInfoStaticContainer.getMetaInfoForLayout();
if (metaInfo != null) {
canvas.setProperty(Property.META_INFO, metaInfo);
}
}
boolean regenerateWidget() {
if (fontRequiredForRegeneration()) {
return false;
}
if (!isFieldRegenerationEnabled()) {
return false;
}
if (parent == null) {
return true;
}
final PdfName type = parent.getFormType();
retrieveStyles();
if ((PdfName.Ch.equals(type) && parent.getFieldFlag(PdfChoiceFormField.FF_COMBO))
|| this.isCombTextFormField()) {
if (parent.getFieldFlag(PdfChoiceFormField.FF_COMBO) && formFieldElement != null) {
drawComboBoxAndSaveAppearance();
return true;
}
return TextAndChoiceLegacyDrawer.regenerateTextAndChoiceField(this);
} else if (PdfName.Ch.equals(type) && !parent.getFieldFlag(PdfChoiceFormField.FF_COMBO)) {
if (formFieldElement != null) {
drawListFormFieldAndSaveAppearance();
return true;
} else {
return TextAndChoiceLegacyDrawer.regenerateTextAndChoiceField(this);
}
} else if (PdfName.Tx.equals(type)) {
drawTextFormFieldAndSaveAppearance();
return true;
} else if (PdfName.Btn.equals(type)) {
if (parent.getFieldFlag(PdfButtonFormField.FF_PUSH_BUTTON)) {
drawPushButtonFieldAndSaveAppearance();
} else if (parent.getFieldFlag(PdfButtonFormField.FF_RADIO)) {
drawRadioButtonAndSaveAppearance(getRadioButtonValue());
} else {
drawCheckBoxAndSaveAppearance(getCheckBoxValue());
}
return true;
} else if (PdfName.Sig.equals(type)) {
drawSignatureFormFieldAndSaveAppearance();
return true;
}
return false;
}
private boolean fontRequiredForRegeneration() {
if (getFont() != null) {
return false;
}
if (parent instanceof PdfButtonFormField) {
return ((PdfButtonFormField) parent).isPushButton();
}
if (parent instanceof PdfSignatureFormField) {
return false;
}
return true;
}
void createInputButton() {
if (!(formFieldElement instanceof Button)) {
// Create it one time and re-set properties during each widget regeneration.
formFieldElement = new Button(parent.getPartialFieldName().toUnicodeString());
}
((Button) formFieldElement).setFont(getFont());
((Button) formFieldElement).setFontSize(getFontSize(getPdfObject()
.getAsArray(PdfName.Rect), parent.getDisplayValue()));
if (getColor() != null) {
((Button) formFieldElement).setFontColor(color);
}
}
void createSigField() {
if (!(formFieldElement instanceof SignatureFieldAppearance)) {
// Create it one time and re-set properties during each widget regeneration.
formFieldElement = new SignatureFieldAppearance(parent.getPartialFieldName().toUnicodeString());
}
if (formFieldElement.