org.apache.pdfbox.pdmodel.interactive.form.PDAppearance 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.cos.COSStream;
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.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.util.PDFOperator;
/**
* This one took me a while, but i'm proud to say that it handles the appearance of a textbox. This allows you to apply
* a value to a field in the document and handle the appearance so that the value is actually visible too. The problem
* was described by Ben Litchfield, the author of the example: org.apache.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is
* the solution.
*
* @author sug
* @author Ben Litchfield
* @version $Revision: 1.20 $
*/
public class PDAppearance
{
private final PDVariableText parent;
private String value;
private final COSString defaultAppearance;
private final PDAcroForm acroForm;
private List widgets = new ArrayList();
/**
* 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 String HIGHLIGHT_COLOR = "0.600006 0.756866 0.854904 rg";
/**
* The default padding.
*
* Adobe adds a default padding of 1 to the widgets bounding box.
*
*/
private static final int DEFAULT_PADDING = 1;
/**
* The padding area.
*
* The box from where he padding into the content area will be calculated.
*
* The default value is to do a padding of 1 on each side of the widgets bounding box.
*
* This might be overwritten by a new setting within the BMC/EMC sequence
*/
private PDRectangle paddingEdge = null;
/**
* The content area.
*
* The inner box where the content will be printed The default value is to do a padding of 1 on each side of the
* paddingEdge.
*
* This might be overwritten by a new setting within the BMC/EMC sequence
*/
private PDRectangle contentArea = null;
/**
* Constructs a COSAppearnce from the given field.
*
* @param theAcroForm
* the acro form that this field is part of.
* @param field
* the field which you wish to control the appearance of
* @throws IOException
* If there is an error creating the appearance.
*/
public PDAppearance(PDAcroForm theAcroForm, PDVariableText field) throws IOException
{
acroForm = theAcroForm;
parent = field;
widgets = field.getKids();
if (widgets == null)
{
widgets = new ArrayList();
widgets.add(field.getWidget());
}
defaultAppearance = getDefaultAppearance();
}
/**
* Returns the default appearance of a textbox. If the textbox does not have one, then it will be taken from the
* AcroForm.
*
* @return The DA element
*/
private COSString getDefaultAppearance()
{
COSString dap = parent.getDefaultAppearance();
if (dap == null)
{
COSArray kids = (COSArray) parent.getDictionary().getDictionaryObject(COSName.KIDS);
if (kids != null && kids.size() > 0)
{
COSDictionary firstKid = (COSDictionary) kids.getObject(0);
dap = (COSString) firstKid.getDictionaryObject(COSName.DA);
}
if (dap == null)
{
dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.DA);
}
}
return dap;
}
private int getQ()
{
int q = parent.getQ();
if (parent.getDictionary().getDictionaryObject(COSName.Q) == null)
{
COSArray kids = (COSArray) parent.getDictionary().getDictionaryObject(COSName.KIDS);
if (kids != null && kids.size() > 0)
{
COSDictionary firstKid = (COSDictionary) kids.getObject(0);
COSNumber qNum = (COSNumber) firstKid.getDictionaryObject(COSName.Q);
if (qNum != null)
{
q = qNum.intValue();
}
}
}
return q;
}
/**
* Extracts the original appearance stream into a list of tokens.
*
* @return The tokens in the original appearance stream
*/
private List getStreamTokens(PDAppearanceStream appearanceStream) throws IOException
{
List tokens = null;
if (appearanceStream != null)
{
tokens = getStreamTokens(appearanceStream.getStream());
}
return tokens;
}
private List getStreamTokens(COSString string) throws IOException
{
PDFStreamParser parser;
List tokens = null;
if (string != null)
{
ByteArrayInputStream stream = new ByteArrayInputStream(string.getBytes());
parser = new PDFStreamParser(stream, acroForm.getDocument().getDocument().getScratchFile());
parser.parse();
tokens = parser.getTokens();
}
return tokens;
}
private List getStreamTokens(COSStream stream) throws IOException
{
PDFStreamParser parser;
List tokens = null;
if (stream != null)
{
parser = new PDFStreamParser(stream);
parser.parse();
tokens = parser.getTokens();
}
return tokens;
}
/**
* Tests if the apperance stream already contains content.
*
* @return true if it contains any content
*/
private boolean containsMarkedContent(List stream)
{
return stream.contains(PDFOperator.getOperator("BMC"));
}
/**
* Apply padding to a rectangle.
*
* Padding is used to create different boxes within the widgets 'box model'.
*
* @return a new rectangle with padding applied
*/
private PDRectangle applyPadding(PDRectangle bbox, float padding)
{
PDRectangle area = new PDRectangle(bbox.getCOSArray());
area.setLowerLeftX(area.getLowerLeftX() + padding);
area.setLowerLeftY(area.getLowerLeftY() + padding);
area.setUpperRightX(area.getUpperRightX() - padding);
area.setUpperRightY(area.getUpperRightY() - padding);
return area;
}
/**
* 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;
Iterator widgetIter = widgets.iterator();
while (widgetIter.hasNext())
{
COSObjectable next = widgetIter.next();
PDField field = null;
PDAnnotationWidget widget;
if (next instanceof PDField)
{
field = (PDField) next;
widget = field.getWidget();
}
else
{
widget = (PDAnnotationWidget) next;
}
PDFormFieldAdditionalActions actions = null;
if (field != null)
{
actions = field.getActions();
}
if (actions != null && actions.getF() != null
&& widget.getDictionary().getDictionaryObject(COSName.AP) == null)
{
// do nothing because the field will be formatted by acrobat
// when it is opened. See FreedomExpressions.pdf for an example of this.
}
else
{
PDAppearanceDictionary appearance = widget.getAppearance();
if (appearance == null)
{
appearance = new PDAppearanceDictionary();
widget.setAppearance(appearance);
}
Map normalAppearance = appearance.getNormalAppearance();
PDAppearanceStream appearanceStream = (PDAppearanceStream) normalAppearance.get("default");
if (appearanceStream == null)
{
COSStream cosStream = acroForm.getDocument().getDocument().createCOSStream();
appearanceStream = new PDAppearanceStream(cosStream);
appearanceStream.setBoundingBox(widget.getRectangle().createRetranslatedRectangle());
appearance.setNormalAppearance(appearanceStream);
}
List tokens = getStreamTokens(appearanceStream);
List daTokens = getStreamTokens(getDefaultAppearance());
PDFont pdFont = getFontAndUpdateResources(tokens, appearanceStream);
// Special handling for listboxes to address PDFBOX-2249
// TODO: Shall be addressed properly in a future release
if (parent instanceof PDChoiceField
&& (parent.getFieldFlags() & ((PDChoiceField) parent).FLAG_COMBO) == 0)
{
generateListboxAppearance(widget, pdFont, tokens, daTokens, appearanceStream, value);
}
else
{
if (!containsMarkedContent(tokens))
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
// BJL 9/25/2004 Must prepend existing stream
// because it might have operators to draw things like
// rectangles and such
ContentStreamWriter writer = new ContentStreamWriter(output);
writer.writeTokens(tokens);
output.write(" /Tx BMC\n".getBytes("ISO-8859-1"));
insertGeneratedAppearance(widget, output, pdFont, tokens, appearanceStream);
output.write(" EMC".getBytes("ISO-8859-1"));
writeToStream(output.toByteArray(), appearanceStream);
}
else
{
if (tokens != null)
{
if (daTokens != null)
{
int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC"));
int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC"));
if (bmcIndex != -1 && emcIndex != -1 && emcIndex == bmcIndex + 1)
{
// if the EMC immediately follows the BMC index then should
// insert the daTokens inbetween the two markers.
tokens.addAll(emcIndex, daTokens);
}
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
ContentStreamWriter writer = new ContentStreamWriter(output);
float fontSize = calculateFontSize(pdFont, appearanceStream.getBoundingBox(), tokens, daTokens);
boolean foundString = false;
int indexOfString = -1;
int setFontIndex = tokens.indexOf(PDFOperator.getOperator("Tf"));
tokens.set(setFontIndex - 1, new COSFloat(fontSize));
int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC"));
int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC"));
if (bmcIndex != -1)
{
writer.writeTokens(tokens, 0, bmcIndex + 1);
}
else
{
writer.writeTokens(tokens);
}
output.write("\n".getBytes("ISO-8859-1"));
insertGeneratedAppearance(widget, output, pdFont, tokens, appearanceStream);
if (emcIndex != -1)
{
writer.writeTokens(tokens, emcIndex, tokens.size());
}
writeToStream(output.toByteArray(), appearanceStream);
}
else
{
// hmm?
}
}
}
}
}
}
private void generateListboxAppearance(PDAnnotationWidget fieldWidget, PDFont pdFont, List tokens, List daTokens,
PDAppearanceStream appearanceStream, String fieldValue) throws IOException
{
// create paddingEdge and contentArea from bounding box
// Default the contentArea to the boundingBox
// taking the padding into account
paddingEdge = applyPadding(appearanceStream.getBoundingBox(), DEFAULT_PADDING);
contentArea = applyPadding(paddingEdge, DEFAULT_PADDING);
if (!containsMarkedContent(tokens))
{
ByteArrayOutputStream output = new ByteArrayOutputStream();
// BJL 9/25/2004 Must prepend existing stream
// because it might have operators to draw things like
// rectangles and such
ContentStreamWriter writer = new ContentStreamWriter(output);
writer.writeTokens(tokens);
output.write(" /Tx BMC\n".getBytes("ISO-8859-1"));
insertGeneratedListboxAppearance(fieldWidget, output, pdFont, tokens, appearanceStream);
output.write(" EMC".getBytes("ISO-8859-1"));
writeToStream(output.toByteArray(), appearanceStream);
}
else
{
if (tokens != null)
{
if (daTokens != null)
{
int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC"));
int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC"));
if (bmcIndex != -1 && emcIndex != -1 && emcIndex == bmcIndex + 1)
{
// if the EMC immediately follows the BMC index then should
// insert the daTokens inbetween the two markers.
tokens.addAll(emcIndex, daTokens);
}
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
ContentStreamWriter writer = new ContentStreamWriter(output);
float fontSize = calculateListboxFontSize(pdFont, appearanceStream.getBoundingBox(), tokens, daTokens);
boolean foundString = false;
int setFontIndex = tokens.indexOf(PDFOperator.getOperator("Tf"));
tokens.set(setFontIndex - 1, new COSFloat(fontSize));
int bmcIndex = tokens.indexOf(PDFOperator.getOperator("BMC"));
/*
* Get the contentArea.
*
* There might be an inner box defined which defines the area where the text is printed. This typically
* looks like ... q 1 1 98 70 re W ...
*/
{
int beginTextIndex = tokens.indexOf(PDFOperator.getOperator("BT"));
if (beginTextIndex != -1)
{
ListIterator innerTokens = tokens.listIterator(bmcIndex);
while (innerTokens.hasNext())
{
if (innerTokens.next() == PDFOperator.getOperator("re")
&& innerTokens.next() == PDFOperator.getOperator("W"))
{
COSArray array = new COSArray();
array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 5));
array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 4));
array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 3));
array.add((COSNumber) tokens.get(innerTokens.previousIndex() - 2));
paddingEdge = new PDRectangle(array);
// as the re operator is using start and width/height adjust the generated
// dimensions
paddingEdge.setUpperRightX(paddingEdge.getLowerLeftX() + paddingEdge.getUpperRightX());
paddingEdge.setUpperRightY(paddingEdge.getLowerLeftY() + paddingEdge.getUpperRightY());
contentArea = applyPadding(paddingEdge, paddingEdge.getLowerLeftX()
- appearanceStream.getBoundingBox().getLowerLeftX());
break;
}
}
}
}
int emcIndex = tokens.indexOf(PDFOperator.getOperator("EMC"));
if (bmcIndex != -1)
{
writer.writeTokens(tokens, 0, bmcIndex + 1);
}
else
{
writer.writeTokens(tokens);
}
output.write("\n".getBytes("ISO-8859-1"));
insertGeneratedListboxAppearance(fieldWidget, output, pdFont, tokens, appearanceStream);
if (emcIndex != -1)
{
writer.writeTokens(tokens, emcIndex, tokens.size());
}
writeToStream(output.toByteArray(), appearanceStream);
}
else
{
// hmm?
}
}
}
private void insertGeneratedAppearance(PDAnnotationWidget fieldWidget, OutputStream output, PDFont pdFont,
List tokens, PDAppearanceStream appearanceStream) throws IOException
{
PrintWriter printWriter = new PrintWriter(output, true);
float fontSize = 0.0f;
PDRectangle boundingBox = appearanceStream.getBoundingBox();
if (boundingBox == null)
{
boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle();
}
// Handle a field with the comb flag being set differently to
// address PDFBOX-91
// TODO: Shall be addressed properly in a future release
if (parent.shouldComb()) {
insertGeneratedPaddingEdge(printWriter, appearanceStream);
}
printWriter.println("BT");
if (defaultAppearance != null)
{
String daString = defaultAppearance.getString();
PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream(daString.getBytes("ISO-8859-1")),
null);
daParser.parse();
List