org.apache.pdfbox.pdmodel.interactive.form.AppearanceGeneratorHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pdfbox Show documentation
Show all versions of pdfbox Show documentation
The Apache PDFBox library is an open source Java tool for working with PDF documents.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.pdmodel.interactive.form;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.util.Matrix;
/**
* Create the AcroForms field appearance helper.
*
* @author Stephan Gerhard
* @author Ben Litchfield
*/
class AppearanceGeneratorHelper
{
private static final Log LOG = LogFactory.getLog(AppearanceGeneratorHelper.class);
private static final Operator BMC = Operator.getOperator("BMC");
private static final Operator EMC = Operator.getOperator("EMC");
private final PDVariableText field;
private PDDefaultAppearanceString defaultAppearance;
private String value;
/**
* The highlight color
*
* The color setting is used by Adobe to display the highlight box for selected entries in a list box.
*
* Regardless of other settings in an existing appearance stream Adobe will always use this value.
*/
private static final int[] HIGHLIGHT_COLOR = {153,193,215};
/**
* The scaling factor for font units to PDF units
*/
private static final int FONTSCALE = 1000;
/**
* The default font size used for multiline text
*/
private static final float DEFAULT_FONT_SIZE = 12;
/**
* The default padding applied by Acrobat to the fields bbox.
*/
private static final float DEFAULT_PADDING = 0.5f;
/**
* Constructs a COSAppearance from the given field.
*
* @param field the field which you wish to control the appearance of
* @throws IOException
*/
AppearanceGeneratorHelper(PDVariableText field) throws IOException
{
this.field = field;
validateAndEnsureAcroFormResources();
this.defaultAppearance = field.getDefaultAppearanceString();
}
/*
* Adobe Reader/Acrobat are adding resources which are at the field/widget level
* to the AcroForm level.
*/
private void validateAndEnsureAcroFormResources() {
// add font resources which might be available at the field
// level but are not at the AcroForm level to the AcroForm
// to match Adobe Reader/Acrobat behavior
if (field.getAcroForm().getDefaultResources() == null)
{
return;
}
PDResources acroFormResources = field.getAcroForm().getDefaultResources();
for (PDAnnotationWidget widget : field.getWidgets())
{
if (widget.getNormalAppearanceStream() != null && widget.getNormalAppearanceStream().getResources() != null)
{
PDResources widgetResources = widget.getNormalAppearanceStream().getResources();
for (COSName fontResourceName : widgetResources.getFontNames())
{
try
{
if (acroFormResources.getFont(fontResourceName) == null)
{
LOG.debug("Adding font resource " + fontResourceName + " from widget to AcroForm");
acroFormResources.put(fontResourceName, widgetResources.getFont(fontResourceName));
}
}
catch (IOException e)
{
LOG.warn("Unable to match field level font with AcroForm font");
}
}
}
}
}
/**
* This is the public method for setting the appearance stream.
*
* @param apValue the String value which the appearance should represent
* @throws IOException If there is an error creating the stream.
*/
public void setAppearanceValue(String apValue) throws IOException
{
value = apValue;
// Treat multiline field values in single lines as single lime values.
// This is in line with how Adobe Reader behaves when enetring text
// interactively but NOT how it behaves when the field value has been
// set programmatically and Reader is forced to generate the appearance
// using PDAcroForm.setNeedAppearances
// see PDFBOX-3911
if (field instanceof PDTextField && !((PDTextField) field).isMultiline())
{
value = apValue.replaceAll("\\u000D\\u000A|[\\u000A\\u000B\\u000C\\u000D\\u0085\\u2028\\u2029]", " ");
}
for (PDAnnotationWidget widget : field.getWidgets())
{
// some fields have the /Da at the widget level if the
// widgets differ in layout.
PDDefaultAppearanceString acroFormAppearance = defaultAppearance;
if (widget.getCOSObject().getDictionaryObject(COSName.DA) != null)
{
defaultAppearance = getWidgetDefaultAppearanceString(widget);
}
PDRectangle rect = widget.getRectangle();
if (rect == null)
{
widget.getCOSObject().removeItem(COSName.AP);
LOG.warn("widget of field " + field.getFullyQualifiedName() + " has no rectangle, no appearance stream created");
continue;
}
PDFormFieldAdditionalActions actions = field.getActions();
// in case all tests fail the field will be formatted by acrobat
// when it is opened. See FreedomExpressions.pdf for an example of this.
if (actions == null || actions.getF() == null ||
widget.getCOSObject().getDictionaryObject(COSName.AP) != null)
{
PDAppearanceDictionary appearanceDict = widget.getAppearance();
if (appearanceDict == null)
{
appearanceDict = new PDAppearanceDictionary();
widget.setAppearance(appearanceDict);
}
PDAppearanceEntry appearance = appearanceDict.getNormalAppearance();
// TODO support appearances other than "normal"
PDAppearanceStream appearanceStream;
if (appearance != null && appearance.isStream())
{
appearanceStream = appearance.getAppearanceStream();
}
else
{
appearanceStream = new PDAppearanceStream(field.getAcroForm().getDocument());
// Calculate the entries for the bounding box and the transformation matrix
// settings for the appearance stream
int rotation = resolveRotation(widget);
Matrix matrix = Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0);
Point2D.Float point2D = matrix.transformPoint(rect.getWidth(), rect.getHeight());
PDRectangle bbox = new PDRectangle(Math.abs((float) point2D.getX()), Math.abs((float) point2D.getY()));
appearanceStream.setBBox(bbox);
appearanceStream.setMatrix(calculateMatrix(bbox, rotation));
appearanceStream.setFormType(1);
appearanceStream.setResources(new PDResources());
appearanceDict.setNormalAppearance(appearanceStream);
// TODO support appearances other than "normal"
}
/*
* Adobe Acrobat always recreates the complete appearance stream if there is an appearance characteristics
* entry (the widget dictionaries MK entry). In addition if there is no content yet also create the appearance
* stream from the entries.
*
*/
if (widget.getAppearanceCharacteristics() != null || appearanceStream.getContentStream().getLength() == 0)
{
initializeAppearanceContent(widget, appearanceStream);
}
setAppearanceContent(widget, appearanceStream);
}
// restore the field level appearance
defaultAppearance = acroFormAppearance;
}
}
private PDDefaultAppearanceString getWidgetDefaultAppearanceString(PDAnnotationWidget widget) throws IOException
{
COSString da = (COSString) widget.getCOSObject().getDictionaryObject(COSName.DA);
PDResources dr = field.getAcroForm().getDefaultResources();
return new PDDefaultAppearanceString(da, dr);
}
private int resolveRotation(PDAnnotationWidget widget)
{
PDAppearanceCharacteristicsDictionary characteristicsDictionary = widget.getAppearanceCharacteristics();
if (characteristicsDictionary != null)
{
// 0 is the default value if the R key doesn't exist
return characteristicsDictionary.getRotation();
}
return 0;
}
/**
* Initialize the content of the appearance stream.
*
* Get settings like border style, border width and colors to be used to draw a rectangle and background color
* around the widget
*
* @param widget the field widget
* @param appearanceStream the appearance stream to be used
* @throws IOException in case we can't write to the appearance stream
*/
private void initializeAppearanceContent(PDAnnotationWidget widget, PDAppearanceStream appearanceStream) throws IOException
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
PDPageContentStream contents = new PDPageContentStream(field.getAcroForm().getDocument(),
appearanceStream, output);
PDAppearanceCharacteristicsDictionary appearanceCharacteristics = widget.getAppearanceCharacteristics();
// TODO: support more entries like patterns, etc.
if (appearanceCharacteristics != null)
{
PDColor backgroundColour = appearanceCharacteristics.getBackground();
if (backgroundColour != null)
{
contents.setNonStrokingColor(backgroundColour);
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
contents.addRect(bbox.getLowerLeftX(),bbox.getLowerLeftY(),bbox.getWidth(), bbox.getHeight());
contents.fill();
}
float lineWidth = 0f;
PDColor borderColour = appearanceCharacteristics.getBorderColour();
if (borderColour != null)
{
contents.setStrokingColor(borderColour);
lineWidth = 1f;
}
PDBorderStyleDictionary borderStyle = widget.getBorderStyle();
if (borderStyle != null && borderStyle.getWidth() > 0)
{
lineWidth = borderStyle.getWidth();
}
if (lineWidth > 0 && borderColour != null)
{
if (lineWidth != 1)
{
contents.setLineWidth(lineWidth);
}
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
PDRectangle clipRect = applyPadding(bbox, Math.max(DEFAULT_PADDING, lineWidth/2));
contents.addRect(clipRect.getLowerLeftX(),clipRect.getLowerLeftY(),clipRect.getWidth(), clipRect.getHeight());
contents.closeAndStroke();
}
}
contents.close();
output.close();
writeToStream(output.toByteArray(), appearanceStream);
}
/**
* Parses an appearance stream into tokens.
*/
private List