/*
* 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.Map;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSFloat;
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.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 PDVariableText parent;
private String value;
private COSString defaultAppearance;
private PDAcroForm acroForm;
private List widgets = new ArrayList();
/**
* 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 apperance 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( "Kids" );
if( kids != null && kids.size() > 0 )
{
COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
dap = (COSString)firstKid.getDictionaryObject( "DA" );
}
if( dap == null )
{
dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.getPDFName("DA"));
}
}
return dap;
}
private int getQ()
{
int q = parent.getQ();
if( parent.getDictionary().getDictionaryObject( "Q" ) == null )
{
COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( "Kids" );
if( kids != null && kids.size() > 0 )
{
COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
COSNumber qNum = (COSNumber)firstKid.getDictionaryObject( "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" ) );
}
/**
* This is the public method for setting the appearance stream.
*
* @param apValue the String value which the apperance shoud represent
*
* @throws IOException If there is an error creating the stream.
*/
public void setAppearanceValue(String apValue) throws IOException
{
// MulitLine check and set
if ( parent.isMultiline() && apValue.indexOf('\n') != -1 )
{
apValue = convertToMultiLine( apValue );
}
value = apValue;
Iterator widgetIter = widgets.iterator();
while( widgetIter.hasNext() )
{
Object next = widgetIter.next();
PDField field = null;
PDAnnotationWidget widget = null;
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( "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 = new COSStream( acroForm.getDocument().getDocument().getScratchFile() );
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 );
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, null );
boolean foundString = false;
for( int i=0; i -1 )
{
result.append(line.substring(lastIdx,currIdx));
result.append(" ) Tj\n0 -13 Td\n(");
lastIdx = currIdx + 1;
}
result.append(line.substring(lastIdx));
return result.toString();
}
/**
* Writes the stream to the actual stream in the COSStream.
*
* @throws IOException If there is an error writing to the stream
*/
private void writeToStream( byte[] data, PDAppearanceStream appearanceStream ) throws IOException
{
OutputStream out = appearanceStream.getStream().createUnfilteredStream();
out.write( data );
out.flush();
}
/**
* w in an appearance stream represents the lineWidth.
* @return the linewidth
*/
private float getLineWidth( List tokens )
{
float retval = 1;
if( tokens != null )
{
int btIndex = tokens.indexOf(PDFOperator.getOperator( "BT" ));
int wIndex = tokens.indexOf(PDFOperator.getOperator( "w" ));
//the w should only be used if it is before the first BT.
if( (wIndex > 0) && (wIndex < btIndex) )
{
retval = ((COSNumber)tokens.get(wIndex-1)).floatValue();
}
}
return retval;
}
private PDRectangle getSmallestDrawnRectangle( PDRectangle boundingBox, List tokens )
{
PDRectangle smallest = boundingBox;
for( int i=0; i potentialSmallest.getUpperRightY() )
{
smallest = potentialSmallest;
}
}
}
return smallest;
}
/**
* My "not so great" method for calculating the fontsize.
* It does not work superb, but it handles ok.
* @return the calculated font-size
*
* @throws IOException If there is an error getting the font height.
*/
private float calculateFontSize( PDFont pdFont, PDRectangle boundingBox, List tokens, List daTokens )
throws IOException
{
float fontSize = 0;
if( daTokens != null )
{
//daString looks like "BMC /Helv 3.4 Tf EMC"
int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) );
if(fontIndex != -1 )
{
fontSize = ((COSNumber)daTokens.get(fontIndex-1)).floatValue();
}
}
float widthBasedFontSize = Float.MAX_VALUE;
if( parent.doNotScroll() )
{
//if we don't scroll then we will shrink the font to fit into the text area.
float widthAtFontSize1 = pdFont.getStringWidth( value )/1000.f;
float availableWidth = getAvailableWidth(boundingBox, getLineWidth(tokens));
widthBasedFontSize = availableWidth / widthAtFontSize1;
}
if( fontSize == 0 )
{
float lineWidth = getLineWidth( tokens );
float stringWidth = pdFont.getStringWidth( value );
float height = 0;
if( pdFont instanceof PDSimpleFont )
{
height = ((PDSimpleFont)pdFont).getFontDescriptor().getFontBoundingBox().getHeight();
}
else
{
//now much we can do, so lets assume font is square and use width
//as the height
height = pdFont.getAverageFontWidth();
}
height = height/1000f;
float availHeight = getAvailableHeight( boundingBox, lineWidth );
fontSize = Math.min((availHeight/height), widthBasedFontSize);
}
return fontSize;
}
/**
* Calculates where to start putting the text in the box.
* The positioning is not quite as accurate as when Acrobat
* places the elements, but it works though.
*
* @return the sting for representing the start position of the text
*
* @throws IOException If there is an error calculating the text position.
*/
private String getTextPosition( PDRectangle boundingBox, PDFont pdFont, float fontSize, List tokens )
throws IOException
{
float lineWidth = getLineWidth( tokens );
float pos = 0.0f;
if(parent.isMultiline())
{
int rows = (int) (getAvailableHeight( boundingBox, lineWidth ) / ((int) fontSize));
pos = ((rows)*fontSize)-fontSize;
}
else
{
if( pdFont instanceof PDSimpleFont )
{
//BJL 9/25/2004
//This algorithm is a little bit of black magic. It does
//not appear to be documented anywhere. Through examining a few
//PDF documents and the value that Acrobat places in there I
//have determined that the below method of computing the position
//is correct for certain documents, but maybe not all. It does
//work f1040ez.pdf and Form_1.pdf
PDFontDescriptor fd = ((PDSimpleFont)pdFont).getFontDescriptor();
float bBoxHeight = boundingBox.getHeight();
float fontHeight = fd.getFontBoundingBox().getHeight() + 2 * fd.getDescent();
fontHeight = (fontHeight/1000) * fontSize;
pos = (bBoxHeight - fontHeight)/2;
}
else
{
throw new IOException( "Error: Don't know how to calculate the position for non-simple fonts" );
}
}
PDRectangle innerBox = getSmallestDrawnRectangle( boundingBox, tokens );
float xInset = 2+ 2*(boundingBox.getWidth() - innerBox.getWidth());
return Math.round(xInset) + " "+ pos + " Td";
}
/**
* calculates the available width of the box.
* @return the calculated available width of the box
*/
private float getAvailableWidth( PDRectangle boundingBox, float lineWidth )
{
return boundingBox.getWidth() - 2 * lineWidth;
}
/**
* calculates the available height of the box.
* @return the calculated available height of the box
*/
private float getAvailableHeight( PDRectangle boundingBox, float lineWidth )
{
return boundingBox.getHeight() - 2 * lineWidth;
}
}