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 }