Home » pdfbox-1.1.0-src » org.apache.pdfbox.pdmodel.interactive.form » [javadoc | source]

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
   10    *
   11    * Unless required by applicable law or agreed to in writing, software
   12    * distributed under the License is distributed on an "AS IS" BASIS,
   13    * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   14    * See the License for the specific language governing permissions and
   15    * limitations under the License.
   16    */
   17   package org.apache.pdfbox.pdmodel.interactive.form;
   18   
   19   import java.io.ByteArrayInputStream;
   20   import java.io.ByteArrayOutputStream;
   21   import java.io.IOException;
   22   import java.io.OutputStream;
   23   import java.io.PrintWriter;
   24   
   25   import java.util.ArrayList;
   26   import java.util.Iterator;
   27   import java.util.List;
   28   import java.util.Map;
   29   
   30   import org.apache.pdfbox.cos.COSArray;
   31   import org.apache.pdfbox.cos.COSDictionary;
   32   import org.apache.pdfbox.cos.COSFloat;
   33   import org.apache.pdfbox.cos.COSName;
   34   import org.apache.pdfbox.cos.COSNumber;
   35   import org.apache.pdfbox.cos.COSStream;
   36   import org.apache.pdfbox.cos.COSString;
   37   
   38   import org.apache.pdfbox.pdfparser.PDFStreamParser;
   39   import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
   40   
   41   import org.apache.pdfbox.pdmodel.PDResources;
   42   
   43   import org.apache.pdfbox.pdmodel.common.PDRectangle;
   44   
   45   import org.apache.pdfbox.pdmodel.font.PDFont;
   46   import org.apache.pdfbox.pdmodel.font.PDFontDescriptor;
   47   import org.apache.pdfbox.pdmodel.font.PDSimpleFont;
   48   
   49   import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
   50   import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
   51   import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
   52   import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
   53   
   54   import org.apache.pdfbox.util.PDFOperator;
   55   
   56   /**
   57    * This one took me a while, but i'm proud to say that it handles
   58    * the appearance of a textbox. This allows you to apply a value to
   59    * a field in the document and handle the appearance so that the
   60    * value is actually visible too.
   61    * The problem was described by Ben Litchfield, the author of the
   62    * example: org.apache.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is the
   63    * solution.
   64    *
   65    * @author sug
   66    * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
   67    * @version $Revision: 1.20 $
   68    */
   69   public class PDAppearance
   70   {
   71       private PDVariableText parent;
   72   
   73       private String value;
   74       private COSString defaultAppearance;
   75   
   76       private PDAcroForm acroForm;
   77       private List widgets = new ArrayList();
   78   
   79   
   80       /**
   81        * Constructs a COSAppearnce from the given field.
   82        *
   83        * @param theAcroForm the acro form that this field is part of.
   84        * @param field the field which you wish to control the appearance of
   85        * @throws IOException If there is an error creating the appearance.
   86        */
   87       public PDAppearance( PDAcroForm theAcroForm, PDVariableText field ) throws IOException
   88       {
   89           acroForm = theAcroForm;
   90           parent = field;
   91   
   92           widgets = field.getKids();
   93           if( widgets == null )
   94           {
   95               widgets = new ArrayList();
   96               widgets.add( field.getWidget() );
   97           }
   98   
   99           defaultAppearance = getDefaultAppearance();
  100   
  101   
  102       }
  103   
  104       /**
  105        * Returns the default apperance of a textbox. If the textbox
  106        * does not have one, then it will be taken from the AcroForm.
  107        * @return The DA element
  108        */
  109       private COSString getDefaultAppearance()
  110       {
  111   
  112           COSString dap = parent.getDefaultAppearance();
  113           if (dap == null)
  114           {
  115               COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( "Kids" );
  116               if( kids != null && kids.size() > 0 )
  117               {
  118                   COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
  119                   dap = (COSString)firstKid.getDictionaryObject( "DA" );
  120               }
  121               if( dap == null )
  122               {
  123                   dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.getPDFName("DA"));
  124               }
  125           }
  126           return dap;
  127       }
  128   
  129       private int getQ()
  130       {
  131           int q = parent.getQ();
  132           if( parent.getDictionary().getDictionaryObject( "Q" ) == null )
  133           {
  134               COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( "Kids" );
  135               if( kids != null && kids.size() > 0 )
  136               {
  137                   COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
  138                   COSNumber qNum = (COSNumber)firstKid.getDictionaryObject( "Q" );
  139                   if( qNum != null )
  140                   {
  141                       q = qNum.intValue();
  142                   }
  143               }
  144           }
  145           return q;
  146       }
  147   
  148       /**
  149        * Extracts the original appearance stream into a list of tokens.
  150        *
  151        * @return The tokens in the original appearance stream
  152        */
  153       private List getStreamTokens( PDAppearanceStream appearanceStream ) throws IOException
  154       {
  155           List tokens = null;
  156           if( appearanceStream != null )
  157           {
  158               tokens = getStreamTokens( appearanceStream.getStream() );
  159           }
  160           return tokens;
  161       }
  162   
  163       private List getStreamTokens( COSString string ) throws IOException
  164       {
  165           PDFStreamParser parser;
  166   
  167           List tokens = null;
  168           if( string != null )
  169           {
  170               ByteArrayInputStream stream = new ByteArrayInputStream( string.getBytes() );
  171               parser = new PDFStreamParser( stream, acroForm.getDocument().getDocument().getScratchFile() );
  172               parser.parse();
  173               tokens = parser.getTokens();
  174           }
  175           return tokens;
  176       }
  177   
  178       private List getStreamTokens( COSStream stream ) throws IOException
  179       {
  180           PDFStreamParser parser;
  181   
  182           List tokens = null;
  183           if( stream != null )
  184           {
  185               parser = new PDFStreamParser( stream );
  186               parser.parse();
  187               tokens = parser.getTokens();
  188           }
  189           return tokens;
  190       }
  191   
  192       /**
  193        * Tests if the apperance stream already contains content.
  194        *
  195        * @return true if it contains any content
  196        */
  197       private boolean containsMarkedContent( List stream )
  198       {
  199           return stream.contains( PDFOperator.getOperator( "BMC" ) );
  200       }
  201   
  202       /**
  203        * This is the public method for setting the appearance stream.
  204        *
  205        * @param apValue the String value which the apperance shoud represent
  206        *
  207        * @throws IOException If there is an error creating the stream.
  208        */
  209       public void setAppearanceValue(String apValue) throws IOException
  210       {
  211           // MulitLine check and set
  212           if ( parent.isMultiline() && apValue.indexOf('\n') != -1 )
  213           {
  214               apValue = convertToMultiLine( apValue );
  215           }
  216   
  217           value = apValue;
  218           Iterator widgetIter = widgets.iterator();
  219           while( widgetIter.hasNext() )
  220           {
  221               Object next = widgetIter.next();
  222               PDField field = null;
  223               PDAnnotationWidget widget = null;
  224               if( next instanceof PDField )
  225               {
  226                   field = (PDField)next;
  227                   widget = field.getWidget();
  228               }
  229               else
  230               {
  231                   widget = (PDAnnotationWidget)next;
  232               }
  233               PDFormFieldAdditionalActions actions = null;
  234               if( field != null )
  235               {
  236                   actions = field.getActions();
  237               }
  238               if( actions != null &&
  239                   actions.getF() != null &&
  240                   widget.getDictionary().getDictionaryObject( "AP" ) ==null)
  241               {
  242                   //do nothing because the field will be formatted by acrobat
  243                   //when it is opened.  See FreedomExpressions.pdf for an example of this.
  244               }
  245               else
  246               {
  247   
  248                   PDAppearanceDictionary appearance = widget.getAppearance();
  249                   if( appearance == null )
  250                   {
  251                       appearance = new PDAppearanceDictionary();
  252                       widget.setAppearance( appearance );
  253                   }
  254   
  255                   Map normalAppearance = appearance.getNormalAppearance();
  256                   PDAppearanceStream appearanceStream = (PDAppearanceStream)normalAppearance.get( "default" );
  257                   if( appearanceStream == null )
  258                   {
  259                       COSStream cosStream = new COSStream( acroForm.getDocument().getDocument().getScratchFile() );
  260                       appearanceStream = new PDAppearanceStream( cosStream );
  261                       appearanceStream.setBoundingBox( widget.getRectangle().createRetranslatedRectangle() );
  262                       appearance.setNormalAppearance( appearanceStream );
  263                   }
  264   
  265                   List tokens = getStreamTokens( appearanceStream );
  266                   List daTokens = getStreamTokens( getDefaultAppearance() );
  267                   PDFont pdFont = getFontAndUpdateResources( tokens, appearanceStream );
  268   
  269                   if (!containsMarkedContent( tokens ))
  270                   {
  271                       ByteArrayOutputStream output = new ByteArrayOutputStream();
  272   
  273                       //BJL 9/25/2004 Must prepend existing stream
  274                       //because it might have operators to draw things like
  275                       //rectangles and such
  276                       ContentStreamWriter writer = new ContentStreamWriter( output );
  277                       writer.writeTokens( tokens );
  278   
  279                       output.write( " /Tx BMC\n".getBytes() );
  280                       insertGeneratedAppearance( widget, output, pdFont, tokens, appearanceStream );
  281                       output.write( " EMC".getBytes() );
  282                       writeToStream( output.toByteArray(), appearanceStream );
  283                   }
  284                   else
  285                   {
  286                       if( tokens != null )
  287                       {
  288                           if( daTokens != null )
  289                           {
  290                               int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" ));
  291                               int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" ));
  292                               if( bmcIndex != -1 && emcIndex != -1 &&
  293                                   emcIndex == bmcIndex+1 )
  294                               {
  295                                   //if the EMC immediately follows the BMC index then should
  296                                   //insert the daTokens inbetween the two markers.
  297                                   tokens.addAll( emcIndex, daTokens );
  298                               }
  299                           }
  300                           ByteArrayOutputStream output = new ByteArrayOutputStream();
  301                           ContentStreamWriter writer = new ContentStreamWriter( output );
  302                           float fontSize = calculateFontSize( pdFont, appearanceStream.getBoundingBox(), tokens, null );
  303                           boolean foundString = false;
  304                           for( int i=0; i<tokens.size(); i++ )
  305                           {
  306                               if( tokens.get( i ) instanceof COSString )
  307                               {
  308                                   foundString = true;
  309                                   COSString drawnString =((COSString)tokens.get(i));
  310                                   drawnString.reset();
  311                                   drawnString.append( apValue.getBytes() );
  312                               }
  313                           }
  314                           int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" ));
  315                           tokens.set( setFontIndex-1, new COSFloat( fontSize ) );
  316                           if( foundString )
  317                           {
  318                               writer.writeTokens( tokens );
  319                           }
  320                           else
  321                           {
  322                               int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" ) );
  323                               int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" ) );
  324   
  325                               if( bmcIndex != -1 )
  326                               {
  327                                   writer.writeTokens( tokens, 0, bmcIndex+1 );
  328                               }
  329                               else
  330                               {
  331                                   writer.writeTokens( tokens );
  332                               }
  333                               output.write( "\n".getBytes() );
  334                               insertGeneratedAppearance( widget, output,
  335                                   pdFont, tokens, appearanceStream );
  336                               if( emcIndex != -1 )
  337                               {
  338                                   writer.writeTokens( tokens, emcIndex, tokens.size() );
  339                               }
  340                           }
  341                           writeToStream( output.toByteArray(), appearanceStream );
  342                       }
  343                       else
  344                       {
  345                           //hmm?
  346                       }
  347                   }
  348               }
  349           }
  350       }
  351   
  352       private void insertGeneratedAppearance( PDAnnotationWidget fieldWidget, OutputStream output,
  353           PDFont pdFont, List tokens, PDAppearanceStream appearanceStream ) throws IOException
  354       {
  355           PrintWriter printWriter = new PrintWriter( output, true );
  356           float fontSize = 0.0f;
  357           PDRectangle boundingBox = null;
  358           boundingBox = appearanceStream.getBoundingBox();
  359           if( boundingBox == null )
  360           {
  361               boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle();
  362           }
  363           printWriter.println( "BT" );
  364           if( defaultAppearance != null )
  365           {
  366               String daString = defaultAppearance.getString();
  367               PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream( daString.getBytes() ), null );
  368               daParser.parse();
  369               List daTokens = daParser.getTokens();
  370               fontSize = calculateFontSize( pdFont, boundingBox, tokens, daTokens );
  371               int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) );
  372               if(fontIndex != -1 )
  373               {
  374                   daTokens.set( fontIndex-1, new COSFloat( fontSize ) );
  375               }
  376               ContentStreamWriter daWriter = new ContentStreamWriter(output);
  377               daWriter.writeTokens( daTokens );
  378           }
  379           printWriter.println( getTextPosition( boundingBox, pdFont, fontSize, tokens ) );
  380           int q = getQ();
  381           if( q == PDTextbox.QUADDING_LEFT )
  382           {
  383               //do nothing because left is default
  384           }
  385           else if( q == PDTextbox.QUADDING_CENTERED ||
  386                    q == PDTextbox.QUADDING_RIGHT )
  387           {
  388               float fieldWidth = boundingBox.getWidth();
  389               float stringWidth = (pdFont.getStringWidth( value )/1000)*fontSize;
  390               float adjustAmount = fieldWidth - stringWidth - 4;
  391   
  392               if( q == PDTextbox.QUADDING_CENTERED )
  393               {
  394                   adjustAmount = adjustAmount/2.0f;
  395               }
  396   
  397               printWriter.println( adjustAmount + " 0 Td" );
  398           }
  399           else
  400           {
  401               throw new IOException( "Error: Unknown justification value:" + q );
  402           }
  403           printWriter.println("(" + value + ") Tj");
  404           printWriter.println("ET" );
  405           printWriter.flush();
  406       }
  407   
  408       private PDFont getFontAndUpdateResources( List tokens, PDAppearanceStream appearanceStream ) throws IOException
  409       {
  410   
  411           PDFont retval = null;
  412           PDResources streamResources = appearanceStream.getResources();
  413           PDResources formResources = acroForm.getDefaultResources();
  414           if( formResources != null )
  415           {
  416               if( streamResources == null )
  417               {
  418                   streamResources = new PDResources();
  419                   appearanceStream.setResources( streamResources );
  420               }
  421   
  422               COSString da = getDefaultAppearance();
  423               if( da != null )
  424               {
  425                   String data = da.getString();
  426                   PDFStreamParser streamParser = new PDFStreamParser(
  427                           new ByteArrayInputStream( data.getBytes() ), null );
  428                   streamParser.parse();
  429                   tokens = streamParser.getTokens();
  430               }
  431   
  432               int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" ));
  433               COSName cosFontName = (COSName)tokens.get( setFontIndex-2 );
  434               String fontName = cosFontName.getName();
  435               retval = (PDFont)streamResources.getFonts().get( fontName );
  436               if( retval == null )
  437               {
  438                   retval = (PDFont)formResources.getFonts().get( fontName );
  439                   streamResources.getFonts().put( fontName, retval );
  440               }
  441           }
  442           return retval;
  443       }
  444   
  445       private String convertToMultiLine( String line )
  446       {
  447           int currIdx = 0;
  448           int lastIdx = 0;
  449           StringBuffer result = new StringBuffer(line.length() + 64);
  450           while( (currIdx = line.indexOf('\n',lastIdx )) > -1 )
  451           {
  452               result.append(line.substring(lastIdx,currIdx));
  453               result.append(" ) Tj\n0 -13 Td\n(");
  454               lastIdx = currIdx + 1;
  455           }
  456           result.append(line.substring(lastIdx));
  457           return result.toString();
  458       }
  459   
  460       /**
  461        * Writes the stream to the actual stream in the COSStream.
  462        *
  463        * @throws IOException If there is an error writing to the stream
  464        */
  465       private void writeToStream( byte[] data, PDAppearanceStream appearanceStream ) throws IOException
  466       {
  467           OutputStream out = appearanceStream.getStream().createUnfilteredStream();
  468           out.write( data );
  469           out.flush();
  470       }
  471   
  472   
  473       /**
  474        * w in an appearance stream represents the lineWidth.
  475        * @return the linewidth
  476        */
  477       private float getLineWidth( List tokens )
  478       {
  479   
  480           float retval = 1;
  481           if( tokens != null )
  482           {
  483               int btIndex = tokens.indexOf(PDFOperator.getOperator( "BT" ));
  484               int wIndex = tokens.indexOf(PDFOperator.getOperator( "w" ));
  485               //the w should only be used if it is before the first BT.
  486               if( (wIndex > 0) && (wIndex < btIndex) )
  487               {
  488                   retval = ((COSNumber)tokens.get(wIndex-1)).floatValue();
  489               }
  490           }
  491           return retval;
  492       }
  493   
  494       private PDRectangle getSmallestDrawnRectangle( PDRectangle boundingBox, List tokens )
  495       {
  496           PDRectangle smallest = boundingBox;
  497           for( int i=0; i<tokens.size(); i++ )
  498           {
  499               Object next = tokens.get( i );
  500               if( next == PDFOperator.getOperator( "re" ) )
  501               {
  502                   COSNumber x = (COSNumber)tokens.get( i-4 );
  503                   COSNumber y = (COSNumber)tokens.get( i-3 );
  504                   COSNumber width = (COSNumber)tokens.get( i-2 );
  505                   COSNumber height = (COSNumber)tokens.get( i-1 );
  506                   PDRectangle potentialSmallest = new PDRectangle();
  507                   potentialSmallest.setLowerLeftX( x.floatValue() );
  508                   potentialSmallest.setLowerLeftY( y.floatValue() );
  509                   potentialSmallest.setUpperRightX( x.floatValue() + width.floatValue() );
  510                   potentialSmallest.setUpperRightY( y.floatValue() + height.floatValue() );
  511                   if( smallest == null ||
  512                       smallest.getLowerLeftX() < potentialSmallest.getLowerLeftX() ||
  513                       smallest.getUpperRightY() > potentialSmallest.getUpperRightY() )
  514                   {
  515                       smallest = potentialSmallest;
  516                   }
  517   
  518               }
  519           }
  520           return smallest;
  521       }
  522   
  523       /**
  524        * My "not so great" method for calculating the fontsize.
  525        * It does not work superb, but it handles ok.
  526        * @return the calculated font-size
  527        *
  528        * @throws IOException If there is an error getting the font height.
  529        */
  530       private float calculateFontSize( PDFont pdFont, PDRectangle boundingBox, List tokens, List daTokens )
  531           throws IOException
  532       {
  533           float fontSize = 0;
  534           if( daTokens != null )
  535           {
  536               //daString looks like   "BMC /Helv 3.4 Tf EMC"
  537   
  538               int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) );
  539               if(fontIndex != -1 )
  540               {
  541                   fontSize = ((COSNumber)daTokens.get(fontIndex-1)).floatValue();
  542               }
  543           }
  544           if( parent.doNotScroll() )
  545           {
  546               //if we don't scroll then we will shrink the font to fit into the text area.
  547               float widthAtFontSize1 = pdFont.getStringWidth( value );
  548               float availableWidth = boundingBox.getWidth();
  549               float perfectFitFontSize = availableWidth / widthAtFontSize1;
  550           }
  551           else if( fontSize == 0 )
  552           {
  553               float lineWidth = getLineWidth( tokens );
  554               float stringWidth = pdFont.getStringWidth( value );
  555               float height = 0;
  556               if( pdFont instanceof PDSimpleFont )
  557               {
  558                   height = ((PDSimpleFont)pdFont).getFontDescriptor().getFontBoundingBox().getHeight();
  559               }
  560               else
  561               {
  562                   //now much we can do, so lets assume font is square and use width
  563                   //as the height
  564                   height = pdFont.getAverageFontWidth();
  565               }
  566               height = height/1000f;
  567   
  568               float availHeight = getAvailableHeight( boundingBox, lineWidth );
  569               fontSize =(availHeight/height);
  570           }
  571           return fontSize;
  572       }
  573   
  574       /**
  575        * Calculates where to start putting the text in the box.
  576        * The positioning is not quite as accurate as when Acrobat
  577        * places the elements, but it works though.
  578        *
  579        * @return the sting for representing the start position of the text
  580        *
  581        * @throws IOException If there is an error calculating the text position.
  582        */
  583       private String getTextPosition( PDRectangle boundingBox, PDFont pdFont, float fontSize, List tokens )
  584           throws IOException
  585       {
  586           float lineWidth = getLineWidth( tokens );
  587           float pos = 0.0f;
  588           if(parent.isMultiline())
  589           {
  590               int rows = (int) (getAvailableHeight( boundingBox, lineWidth ) / ((int) fontSize));
  591               pos = ((rows)*fontSize)-fontSize;
  592           }
  593           else
  594           {
  595               if( pdFont instanceof PDSimpleFont )
  596               {
  597                   //BJL 9/25/2004
  598                   //This algorithm is a little bit of black magic.  It does
  599                   //not appear to be documented anywhere.  Through examining a few
  600                   //PDF documents and the value that Acrobat places in there I
  601                   //have determined that the below method of computing the position
  602                   //is correct for certain documents, but maybe not all.  It does
  603                   //work f1040ez.pdf and Form_1.pdf
  604                   PDFontDescriptor fd = ((PDSimpleFont)pdFont).getFontDescriptor();
  605                   float bBoxHeight = boundingBox.getHeight();
  606                   float fontHeight = fd.getFontBoundingBox().getHeight() + 2 * fd.getDescent();
  607                   fontHeight = (fontHeight/1000) * fontSize;
  608                   pos = (bBoxHeight - fontHeight)/2;
  609               }
  610               else
  611               {
  612                   throw new IOException( "Error: Don't know how to calculate the position for non-simple fonts" );
  613               }
  614           }
  615           PDRectangle innerBox = getSmallestDrawnRectangle( boundingBox, tokens );
  616           float xInset = 2+ 2*(boundingBox.getWidth() - innerBox.getWidth());
  617           return Math.round(xInset) + " "+ pos + " Td";
  618       }
  619   
  620       /**
  621        * calculates the available width of the box.
  622        * @return the calculated available width of the box
  623        */
  624       private float getAvailableWidth( PDRectangle boundingBox, float lineWidth )
  625       {
  626           return boundingBox.getWidth() - 2 * lineWidth;
  627       }
  628   
  629       /**
  630        * calculates the available height of the box.
  631        * @return the calculated available height of the box
  632        */
  633       private float getAvailableHeight( PDRectangle boundingBox, float lineWidth )
  634       {
  635           return boundingBox.getHeight() - 2 * lineWidth;
  636       }
  637   }

Home » pdfbox-1.1.0-src » org.apache.pdfbox.pdmodel.interactive.form » [javadoc | source]