From 6025b6016517c6d898d8957d1d7e03ba71431912 Mon Sep 17 00:00:00 2001 From: tknall Date: Fri, 1 Dec 2006 12:20:24 +0000 Subject: Initial import of release 2.2. git-svn-id: https://joinup.ec.europa.eu/svn/pdf-as/trunk@4 7b5415b0-85f9-ee4d-85bd-d5d0c3b42d1c --- .../pdmodel/interactive/form/PDAppearance.java | 645 +++++++++++++++++++++ 1 file changed, 645 insertions(+) create mode 100644 src/main/java/org/pdfbox/pdmodel/interactive/form/PDAppearance.java (limited to 'src/main/java/org/pdfbox/pdmodel/interactive/form/PDAppearance.java') diff --git a/src/main/java/org/pdfbox/pdmodel/interactive/form/PDAppearance.java b/src/main/java/org/pdfbox/pdmodel/interactive/form/PDAppearance.java new file mode 100644 index 0000000..ff18c86 --- /dev/null +++ b/src/main/java/org/pdfbox/pdmodel/interactive/form/PDAppearance.java @@ -0,0 +1,645 @@ +/** + * Copyright (c) 2003-2004, www.pdfbox.org + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of pdfbox; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://www.pdfbox.org + * + */ +package org.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.pdfbox.cos.COSArray; +import org.pdfbox.cos.COSDictionary; +import org.pdfbox.cos.COSFloat; +import org.pdfbox.cos.COSName; +import org.pdfbox.cos.COSNumber; +import org.pdfbox.cos.COSStream; +import org.pdfbox.cos.COSString; + +import org.pdfbox.pdfparser.PDFStreamParser; +import org.pdfbox.pdfwriter.ContentStreamWriter; + +import org.pdfbox.pdmodel.PDResources; + +import org.pdfbox.pdmodel.common.PDRectangle; + +import org.pdfbox.pdmodel.font.PDFont; +import org.pdfbox.pdmodel.font.PDFontDescriptor; +import org.pdfbox.pdmodel.font.PDSimpleFont; + +import org.pdfbox.pdmodel.interactive.action.PDAdditionalActions; +import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; + +import org.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.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is the + * solution. + * + * @author sug + * @author Ben Litchfield (ben@benlitchfield.com) + * @version $Revision: 1.17 $ + */ +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(); + PDAnnotationWidget widget = null; + if( next instanceof PDField ) + { + widget = ((PDField)next).getWidget(); + } + else + { + widget = (PDAnnotationWidget)next; + } + PDAdditionalActions actions = widget.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() ); + insertGeneratedAppearance( widget, output, pdFont, tokens, appearanceStream ); + output.write( " EMC".getBytes() ); + 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(value.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(); + } + } + 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 ); + float availableWidth = boundingBox.getWidth(); + float perfectFitFontSize = availableWidth / widthAtFontSize1; + } + else 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 =(availHeight/height); + } + 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; + } +} \ No newline at end of file -- cgit v1.2.3