KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > pdfbox > pdmodel > interactive > form > PDAppearance


1 /**
2  * Copyright (c) 2003-2006, www.pdfbox.org
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice,
9  * this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright notice,
11  * this list of conditions and the following disclaimer in the documentation
12  * and/or other materials provided with the distribution.
13  * 3. Neither the name of pdfbox; nor the names of its
14  * contributors may be used to endorse or promote products derived from this
15  * software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  *
28  * http://www.pdfbox.org
29  *
30  */

31 package org.pdfbox.pdmodel.interactive.form;
32
33 import java.io.ByteArrayInputStream JavaDoc;
34 import java.io.ByteArrayOutputStream JavaDoc;
35 import java.io.IOException JavaDoc;
36 import java.io.OutputStream JavaDoc;
37 import java.io.PrintWriter JavaDoc;
38
39 import java.util.ArrayList JavaDoc;
40 import java.util.Iterator JavaDoc;
41 import java.util.List JavaDoc;
42 import java.util.Map JavaDoc;
43
44 import org.pdfbox.cos.COSArray;
45 import org.pdfbox.cos.COSDictionary;
46 import org.pdfbox.cos.COSFloat;
47 import org.pdfbox.cos.COSName;
48 import org.pdfbox.cos.COSNumber;
49 import org.pdfbox.cos.COSStream;
50 import org.pdfbox.cos.COSString;
51
52 import org.pdfbox.pdfparser.PDFStreamParser;
53 import org.pdfbox.pdfwriter.ContentStreamWriter;
54
55 import org.pdfbox.pdmodel.PDResources;
56
57 import org.pdfbox.pdmodel.common.PDRectangle;
58
59 import org.pdfbox.pdmodel.font.PDFont;
60 import org.pdfbox.pdmodel.font.PDFontDescriptor;
61 import org.pdfbox.pdmodel.font.PDSimpleFont;
62
63 import org.pdfbox.pdmodel.interactive.action.PDAdditionalActions;
64 import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
65 import org.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
66 import org.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
67
68 import org.pdfbox.util.PDFOperator;
69
70 /**
71  * This one took me a while, but i'm proud to say that it handles
72  * the appearance of a textbox. This allows you to apply a value to
73  * a field in the document and handle the appearance so that the
74  * value is actually visible too.
75  * The problem was described by Ben Litchfield, the author of the
76  * example: org.pdfbox.examlpes.fdf.ImportFDF. So Ben, here is the
77  * solution.
78  *
79  * @author sug
80  * @author <a HREF="mailto:ben@benlitchfield.com">Ben Litchfield</a>
81  * @version $Revision: 1.19 $
82  */

83 public class PDAppearance
84 {
85     private PDVariableText parent;
86
87     private String JavaDoc value;
88     private COSString defaultAppearance;
89
90     private PDAcroForm acroForm;
91     private List JavaDoc widgets = new ArrayList JavaDoc();
92
93
94     /**
95      * Constructs a COSAppearnce from the given field.
96      *
97      * @param theAcroForm the acro form that this field is part of.
98      * @param field the field which you wish to control the appearance of
99      * @throws IOException If there is an error creating the appearance.
100      */

101     public PDAppearance( PDAcroForm theAcroForm, PDVariableText field ) throws IOException JavaDoc
102     {
103         acroForm = theAcroForm;
104         parent = field;
105         
106         widgets = field.getKids();
107         if( widgets == null )
108         {
109             widgets = new ArrayList JavaDoc();
110             widgets.add( field.getWidget() );
111         }
112         
113         defaultAppearance = getDefaultAppearance();
114
115         
116     }
117
118     /**
119      * Returns the default apperance of a textbox. If the textbox
120      * does not have one, then it will be taken from the AcroForm.
121      * @return The DA element
122      */

123     private COSString getDefaultAppearance()
124     {
125         
126         COSString dap = parent.getDefaultAppearance();
127         if (dap == null)
128         {
129             COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( "Kids" );
130             if( kids != null && kids.size() > 0 )
131             {
132                 COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
133                 dap = (COSString)firstKid.getDictionaryObject( "DA" );
134             }
135             if( dap == null )
136             {
137                 dap = (COSString) acroForm.getDictionary().getDictionaryObject(COSName.getPDFName("DA"));
138             }
139         }
140         return dap;
141     }
142     
143     private int getQ()
144     {
145         int q = parent.getQ();
146         if( parent.getDictionary().getDictionaryObject( "Q" ) == null )
147         {
148             COSArray kids = (COSArray)parent.getDictionary().getDictionaryObject( "Kids" );
149             if( kids != null && kids.size() > 0 )
150             {
151                 COSDictionary firstKid = (COSDictionary)kids.getObject( 0 );
152                 COSNumber qNum = (COSNumber)firstKid.getDictionaryObject( "Q" );
153                 if( qNum != null )
154                 {
155                     q = qNum.intValue();
156                 }
157             }
158         }
159         return q;
160     }
161
162     /**
163      * Extracts the original appearance stream into a list of tokens.
164      *
165      * @return The tokens in the original appearance stream
166      */

167     private List JavaDoc getStreamTokens( PDAppearanceStream appearanceStream ) throws IOException JavaDoc
168     {
169         List JavaDoc tokens = null;
170         if( appearanceStream != null )
171         {
172             tokens = getStreamTokens( appearanceStream.getStream() );
173         }
174         return tokens;
175     }
176     
177     private List JavaDoc getStreamTokens( COSString string ) throws IOException JavaDoc
178     {
179         PDFStreamParser parser;
180
181         List JavaDoc tokens = null;
182         if( string != null )
183         {
184             ByteArrayInputStream JavaDoc stream = new ByteArrayInputStream JavaDoc( string.getBytes() );
185             parser = new PDFStreamParser( stream, acroForm.getDocument().getDocument().getScratchFile() );
186             parser.parse();
187             tokens = parser.getTokens();
188         }
189         return tokens;
190     }
191     
192     private List JavaDoc getStreamTokens( COSStream stream ) throws IOException JavaDoc
193     {
194         PDFStreamParser parser;
195
196         List JavaDoc tokens = null;
197         if( stream != null )
198         {
199             parser = new PDFStreamParser( stream );
200             parser.parse();
201             tokens = parser.getTokens();
202         }
203         return tokens;
204     }
205
206     /**
207      * Tests if the apperance stream already contains content.
208      *
209      * @return true if it contains any content
210      */

211     private boolean containsMarkedContent( List JavaDoc stream )
212     {
213         return stream.contains( PDFOperator.getOperator( "BMC" ) );
214     }
215
216     /**
217      * This is the public method for setting the appearance stream.
218      *
219      * @param apValue the String value which the apperance shoud represent
220      *
221      * @throws IOException If there is an error creating the stream.
222      */

223     public void setAppearanceValue(String JavaDoc apValue) throws IOException JavaDoc
224     {
225         // MulitLine check and set
226
if ( parent.isMultiline() && apValue.indexOf('\n') != -1 )
227         {
228             apValue = convertToMultiLine( apValue );
229         }
230
231         value = apValue;
232         Iterator JavaDoc widgetIter = widgets.iterator();
233         while( widgetIter.hasNext() )
234         {
235             Object JavaDoc next = widgetIter.next();
236             PDAnnotationWidget widget = null;
237             if( next instanceof PDField )
238             {
239                 widget = ((PDField)next).getWidget();
240             }
241             else
242             {
243                 widget = (PDAnnotationWidget)next;
244             }
245             PDAdditionalActions actions = widget.getActions();
246             if( actions != null &&
247                 actions.getF() != null &&
248                 widget.getDictionary().getDictionaryObject( "AP" ) ==null)
249             {
250                 //do nothing because the field will be formatted by acrobat
251
//when it is opened. See FreedomExpressions.pdf for an example of this.
252
}
253             else
254             {
255             
256                 PDAppearanceDictionary appearance = widget.getAppearance();
257                 if( appearance == null )
258                 {
259                     appearance = new PDAppearanceDictionary();
260                     widget.setAppearance( appearance );
261                 }
262     
263                 Map JavaDoc normalAppearance = appearance.getNormalAppearance();
264                 PDAppearanceStream appearanceStream = (PDAppearanceStream)normalAppearance.get( "default" );
265                 if( appearanceStream == null )
266                 {
267                     COSStream cosStream = new COSStream( acroForm.getDocument().getDocument().getScratchFile() );
268                     appearanceStream = new PDAppearanceStream( cosStream );
269                     appearanceStream.setBoundingBox( widget.getRectangle().createRetranslatedRectangle() );
270                     appearance.setNormalAppearance( appearanceStream );
271                 }
272                 
273                 List JavaDoc tokens = getStreamTokens( appearanceStream );
274                 List JavaDoc daTokens = getStreamTokens( getDefaultAppearance() );
275                 PDFont pdFont = getFontAndUpdateResources( tokens, appearanceStream );
276                 
277                 if (!containsMarkedContent( tokens ))
278                 {
279                     ByteArrayOutputStream JavaDoc output = new ByteArrayOutputStream JavaDoc();
280         
281                     //BJL 9/25/2004 Must prepend existing stream
282
//because it might have operators to draw things like
283
//rectangles and such
284
ContentStreamWriter writer = new ContentStreamWriter( output );
285                     writer.writeTokens( tokens );
286         
287                     output.write( " /Tx BMC\n".getBytes() );
288                     insertGeneratedAppearance( widget, output, pdFont, tokens, appearanceStream );
289                     output.write( " EMC".getBytes() );
290                     writeToStream( output.toByteArray(), appearanceStream );
291                 }
292                 else
293                 {
294                     if( tokens != null )
295                     {
296                         if( daTokens != null )
297                         {
298                             int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" ));
299                             int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" ));
300                             if( bmcIndex != -1 && emcIndex != -1 &&
301                                 emcIndex == bmcIndex+1 )
302                             {
303                                 //if the EMC immediately follows the BMC index then should
304
//insert the daTokens inbetween the two markers.
305
tokens.addAll( emcIndex, daTokens );
306                             }
307                         }
308                         ByteArrayOutputStream JavaDoc output = new ByteArrayOutputStream JavaDoc();
309                         ContentStreamWriter writer = new ContentStreamWriter( output );
310                         float fontSize = calculateFontSize( pdFont, appearanceStream.getBoundingBox(), tokens, null );
311                         boolean foundString = false;
312                         for( int i=0; i<tokens.size(); i++ )
313                         {
314                             if( tokens.get( i ) instanceof COSString )
315                             {
316                                 foundString = true;
317                                 COSString drawnString =((COSString)tokens.get(i));
318                                 drawnString.reset();
319                                 drawnString.append( apValue.getBytes() );
320                             }
321                         }
322                         int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" ));
323                         tokens.set( setFontIndex-1, new COSFloat( fontSize ) );
324                         if( foundString )
325                         {
326                             writer.writeTokens( tokens );
327                         }
328                         else
329                         {
330                             int bmcIndex = tokens.indexOf( PDFOperator.getOperator( "BMC" ) );
331                             int emcIndex = tokens.indexOf( PDFOperator.getOperator( "EMC" ) );
332     
333                             if( bmcIndex != -1 )
334                             {
335                                 writer.writeTokens( tokens, 0, bmcIndex+1 );
336                             }
337                             else
338                             {
339                                 writer.writeTokens( tokens );
340                             }
341                             output.write( "\n".getBytes() );
342                             insertGeneratedAppearance( widget, output,
343                                 pdFont, tokens, appearanceStream );
344                             if( emcIndex != -1 )
345                             {
346                                 writer.writeTokens( tokens, emcIndex, tokens.size() );
347                             }
348                         }
349                         writeToStream( output.toByteArray(), appearanceStream );
350                     }
351                     else
352                     {
353                         //hmm?
354
}
355                 }
356             }
357         }
358     }
359
360     private void insertGeneratedAppearance( PDAnnotationWidget fieldWidget, OutputStream JavaDoc output,
361         PDFont pdFont, List JavaDoc tokens, PDAppearanceStream appearanceStream ) throws IOException JavaDoc
362     {
363         PrintWriter JavaDoc printWriter = new PrintWriter JavaDoc( output, true );
364         float fontSize = 0.0f;
365         PDRectangle boundingBox = null;
366         boundingBox = appearanceStream.getBoundingBox();
367         if( boundingBox == null )
368         {
369             boundingBox = fieldWidget.getRectangle().createRetranslatedRectangle();
370         }
371         printWriter.println( "BT" );
372         if( defaultAppearance != null )
373         {
374             String JavaDoc daString = defaultAppearance.getString();
375             PDFStreamParser daParser = new PDFStreamParser(new ByteArrayInputStream JavaDoc( daString.getBytes() ), null );
376             daParser.parse();
377             List JavaDoc daTokens = daParser.getTokens();
378             fontSize = calculateFontSize( pdFont, boundingBox, tokens, daTokens );
379             int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) );
380             if(fontIndex != -1 )
381             {
382                 daTokens.set( fontIndex-1, new COSFloat( fontSize ) );
383             }
384             ContentStreamWriter daWriter = new ContentStreamWriter(output);
385             daWriter.writeTokens( daTokens );
386         }
387         printWriter.println( getTextPosition( boundingBox, pdFont, fontSize, tokens ) );
388         int q = getQ();
389         if( q == PDTextbox.QUADDING_LEFT )
390         {
391             //do nothing because left is default
392
}
393         else if( q == PDTextbox.QUADDING_CENTERED ||
394                  q == PDTextbox.QUADDING_RIGHT )
395         {
396             float fieldWidth = boundingBox.getWidth();
397             float stringWidth = (pdFont.getStringWidth( value )/1000)*fontSize;
398             float adjustAmount = fieldWidth - stringWidth - 4;
399
400             if( q == PDTextbox.QUADDING_CENTERED )
401             {
402                 adjustAmount = adjustAmount/2.0f;
403             }
404
405             printWriter.println( adjustAmount + " 0 Td" );
406         }
407         else
408         {
409             throw new IOException JavaDoc( "Error: Unknown justification value:" + q );
410         }
411         printWriter.println("(" + value + ") Tj");
412         printWriter.println("ET" );
413         printWriter.flush();
414     }
415
416     private PDFont getFontAndUpdateResources( List JavaDoc tokens, PDAppearanceStream appearanceStream ) throws IOException JavaDoc
417     {
418
419         PDFont retval = null;
420         PDResources streamResources = appearanceStream.getResources();
421         PDResources formResources = acroForm.getDefaultResources();
422         if( formResources != null )
423         {
424             if( streamResources == null )
425             {
426                 streamResources = new PDResources();
427                 appearanceStream.setResources( streamResources );
428             }
429             
430             COSString da = getDefaultAppearance();
431             if( da != null )
432             {
433                 String JavaDoc data = da.getString();
434                 PDFStreamParser streamParser = new PDFStreamParser(
435                         new ByteArrayInputStream JavaDoc( data.getBytes() ), null );
436                 streamParser.parse();
437                 tokens = streamParser.getTokens();
438             }
439
440             int setFontIndex = tokens.indexOf( PDFOperator.getOperator( "Tf" ));
441             COSName cosFontName = (COSName)tokens.get( setFontIndex-2 );
442             String JavaDoc fontName = cosFontName.getName();
443             retval = (PDFont)streamResources.getFonts().get( fontName );
444             if( retval == null )
445             {
446                 retval = (PDFont)formResources.getFonts().get( fontName );
447                 streamResources.getFonts().put( fontName, retval );
448             }
449         }
450         return retval;
451     }
452
453     private String JavaDoc convertToMultiLine( String JavaDoc line )
454     {
455         int currIdx = 0;
456         int lastIdx = 0;
457         StringBuffer JavaDoc result = new StringBuffer JavaDoc(line.length() + 64);
458         while( (currIdx = line.indexOf('\n',lastIdx )) > -1 )
459         {
460             result.append(line.substring(lastIdx,currIdx));
461             result.append(" ) Tj\n0 -13 Td\n(");
462             lastIdx = currIdx + 1;
463         }
464         result.append(line.substring(lastIdx));
465         return result.toString();
466     }
467
468     /**
469      * Writes the stream to the actual stream in the COSStream.
470      *
471      * @throws IOException If there is an error writing to the stream
472      */

473     private void writeToStream( byte[] data, PDAppearanceStream appearanceStream ) throws IOException JavaDoc
474     {
475         OutputStream JavaDoc out = appearanceStream.getStream().createUnfilteredStream();
476         out.write( data );
477         out.flush();
478     }
479
480
481     /**
482      * w in an appearance stream represents the lineWidth.
483      * @return the linewidth
484      */

485     private float getLineWidth( List JavaDoc tokens )
486     {
487         
488         float retval = 1;
489         if( tokens != null )
490         {
491             int btIndex = tokens.indexOf(PDFOperator.getOperator( "BT" ));
492             int wIndex = tokens.indexOf(PDFOperator.getOperator( "w" ));
493             //the w should only be used if it is before the first BT.
494
if( (wIndex > 0) && (wIndex < btIndex) )
495             {
496                 retval = ((COSNumber)tokens.get(wIndex-1)).floatValue();
497             }
498         }
499         return retval;
500     }
501     
502     private PDRectangle getSmallestDrawnRectangle( PDRectangle boundingBox, List JavaDoc tokens )
503     {
504         PDRectangle smallest = boundingBox;
505         for( int i=0; i<tokens.size(); i++ )
506         {
507             Object JavaDoc next = tokens.get( i );
508             if( next == PDFOperator.getOperator( "re" ) )
509             {
510                 COSNumber x = (COSNumber)tokens.get( i-4 );
511                 COSNumber y = (COSNumber)tokens.get( i-3 );
512                 COSNumber width = (COSNumber)tokens.get( i-2 );
513                 COSNumber height = (COSNumber)tokens.get( i-1 );
514                 PDRectangle potentialSmallest = new PDRectangle();
515                 potentialSmallest.setLowerLeftX( x.floatValue() );
516                 potentialSmallest.setLowerLeftY( y.floatValue() );
517                 potentialSmallest.setUpperRightX( x.floatValue() + width.floatValue() );
518                 potentialSmallest.setUpperRightY( y.floatValue() + height.floatValue() );
519                 if( smallest == null ||
520                     smallest.getLowerLeftX() < potentialSmallest.getLowerLeftX() ||
521                     smallest.getUpperRightY() > potentialSmallest.getUpperRightY() )
522                 {
523                     smallest = potentialSmallest;
524                 }
525                 
526             }
527         }
528         return smallest;
529     }
530
531     /**
532      * My "not so great" method for calculating the fontsize.
533      * It does not work superb, but it handles ok.
534      * @return the calculated font-size
535      *
536      * @throws IOException If there is an error getting the font height.
537      */

538     private float calculateFontSize( PDFont pdFont, PDRectangle boundingBox, List JavaDoc tokens, List JavaDoc daTokens )
539         throws IOException JavaDoc
540     {
541         float fontSize = 0;
542         if( daTokens != null )
543         {
544             //daString looks like "BMC /Helv 3.4 Tf EMC"
545

546             int fontIndex = daTokens.indexOf( PDFOperator.getOperator( "Tf" ) );
547             if(fontIndex != -1 )
548             {
549                 fontSize = ((COSNumber)daTokens.get(fontIndex-1)).floatValue();
550             }
551         }
552         if( parent.doNotScroll() )
553         {
554             //if we don't scroll then we will shrink the font to fit into the text area.
555
float widthAtFontSize1 = pdFont.getStringWidth( value );
556             float availableWidth = boundingBox.getWidth();
557             float perfectFitFontSize = availableWidth / widthAtFontSize1;
558         }
559         else if( fontSize == 0 )
560         {
561             float lineWidth = getLineWidth( tokens );
562             float stringWidth = pdFont.getStringWidth( value );
563             float height = 0;
564             if( pdFont instanceof PDSimpleFont )
565             {
566                 height = ((PDSimpleFont)pdFont).getFontDescriptor().getFontBoundingBox().getHeight();
567             }
568             else
569             {
570                 //now much we can do, so lets assume font is square and use width
571
//as the height
572
height = pdFont.getAverageFontWidth();
573             }
574             height = height/1000f;
575     
576             float availHeight = getAvailableHeight( boundingBox, lineWidth );
577             fontSize =(availHeight/height);
578         }
579         return fontSize;
580     }
581
582     /**
583      * Calculates where to start putting the text in the box.
584      * The positioning is not quite as accurate as when Acrobat
585      * places the elements, but it works though.
586      *
587      * @return the sting for representing the start position of the text
588      *
589      * @throws IOException If there is an error calculating the text position.
590      */

591     private String JavaDoc getTextPosition( PDRectangle boundingBox, PDFont pdFont, float fontSize, List JavaDoc tokens )
592         throws IOException JavaDoc
593     {
594         float lineWidth = getLineWidth( tokens );
595         float pos = 0.0f;
596         if(parent.isMultiline())
597         {
598             int rows = (int) (getAvailableHeight( boundingBox, lineWidth ) / ((int) fontSize));
599             pos = ((rows)*fontSize)-fontSize;
600         }
601         else
602         {
603             if( pdFont instanceof PDSimpleFont )
604             {
605                 //BJL 9/25/2004
606
//This algorithm is a little bit of black magic. It does
607
//not appear to be documented anywhere. Through examining a few
608
//PDF documents and the value that Acrobat places in there I
609
//have determined that the below method of computing the position
610
//is correct for certain documents, but maybe not all. It does
611
//work f1040ez.pdf and Form_1.pdf
612
PDFontDescriptor fd = ((PDSimpleFont)pdFont).getFontDescriptor();
613                 float bBoxHeight = boundingBox.getHeight();
614                 float fontHeight = fd.getFontBoundingBox().getHeight() + 2 * fd.getDescent();
615                 fontHeight = (fontHeight/1000) * fontSize;
616                 pos = (bBoxHeight - fontHeight)/2;
617             }
618             else
619             {
620                 throw new IOException JavaDoc( "Error: Don't know how to calculate the position for non-simple fonts" );
621             }
622         }
623         PDRectangle innerBox = getSmallestDrawnRectangle( boundingBox, tokens );
624         float xInset = 2+ 2*(boundingBox.getWidth() - innerBox.getWidth());
625         return Math.round(xInset) + " "+ pos + " Td";
626     }
627     
628     /**
629      * calculates the available width of the box.
630      * @return the calculated available width of the box
631      */

632     private float getAvailableWidth( PDRectangle boundingBox, float lineWidth )
633     {
634         return boundingBox.getWidth() - 2 * lineWidth;
635     }
636
637     /**
638      * calculates the available height of the box.
639      * @return the calculated available height of the box
640      */

641     private float getAvailableHeight( PDRectangle boundingBox, float lineWidth )
642     {
643         return boundingBox.getHeight() - 2 * lineWidth;
644     }
645 }
Popular Tags