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.pdfviewer; 18 19 import java.awt.BasicStroke; 20 import java.awt.Dimension; 21 import java.awt.Graphics; 22 import java.awt.Graphics2D; 23 import java.awt.geom.Area; 24 import java.awt.RenderingHints; 25 import java.awt.geom.AffineTransform; 26 import java.awt.geom.GeneralPath; 27 import java.awt.geom.Point2D; 28 import java.io.IOException; 29 import java.util.List; 30 import java.util.Map; 31 32 import org.apache.commons.logging.Log; 33 import org.apache.commons.logging.LogFactory; 34 import org.apache.pdfbox.pdmodel.PDPage; 35 import org.apache.pdfbox.pdmodel.PDResources; 36 import org.apache.pdfbox.pdmodel.common.PDMatrix; 37 import org.apache.pdfbox.pdmodel.common.PDRectangle; 38 import org.apache.pdfbox.pdmodel.font.PDFont; 39 import org.apache.pdfbox.pdmodel.graphics.PDGraphicsState; 40 import org.apache.pdfbox.pdmodel.graphics.PDShading; 41 import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; 42 import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; 43 import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; 44 import org.apache.pdfbox.pdmodel.text.PDTextState; 45 import org.apache.pdfbox.util.Matrix; 46 import org.apache.pdfbox.util.PDFStreamEngine; 47 import org.apache.pdfbox.util.ResourceLoader; 48 import org.apache.pdfbox.util.TextPosition; 49 import org.apache.pdfbox.cos.COSName; 50 import org.apache.pdfbox.cos.COSBase; 51 import org.apache.pdfbox.cos.COSObject; 52 import org.apache.pdfbox.cos.COSDictionary; 53 54 55 /** 56 * This will paint a page in a PDF document to a graphics context. 57 * 58 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a> 59 * @version $Revision: 1.22 $ 60 */ 61 public class PageDrawer extends PDFStreamEngine 62 { 63 64 /** 65 * Log instance. 66 */ 67 private static final Log log = LogFactory.getLog(PageDrawer.class); 68 69 private Graphics2D graphics; 70 private Dimension pageSize; 71 private PDPage page; 72 73 private GeneralPath linePath = new GeneralPath(); 74 75 /** 76 * Default constructor, loads properties from file. 77 * 78 * @throws IOException If there is an error loading properties from the file. 79 */ 80 public PageDrawer() throws IOException 81 { 82 super( ResourceLoader.loadProperties( "Resources/PageDrawer.properties", true ) ); 83 } 84 85 /** 86 * This will draw the page to the requested context. 87 * 88 * @param g The graphics context to draw onto. 89 * @param p The page to draw. 90 * @param pageDimension The size of the page to draw. 91 * 92 * @throws IOException If there is an IO error while drawing the page. 93 */ 94 public void drawPage( Graphics g, PDPage p, Dimension pageDimension ) throws IOException 95 { 96 graphics = (Graphics2D)g; 97 page = p; 98 pageSize = pageDimension; 99 graphics.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); 100 graphics.setRenderingHint( RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON ); 101 // Only if there is some content, we have to process it. 102 // Otherwise we are done here and we will produce an empty page 103 if ( page.getContents() != null) 104 { 105 PDResources resources = page.findResources(); 106 processStream( page, resources, page.getContents().getStream() ); 107 } 108 List annotations = page.getAnnotations(); 109 for( int i=0; i<annotations.size(); i++ ) 110 { 111 PDAnnotation annot = (PDAnnotation)annotations.get( i ); 112 PDRectangle rect = annot.getRectangle(); 113 String appearanceName = annot.getAppearanceStream(); 114 PDAppearanceDictionary appearDictionary = annot.getAppearance(); 115 if( appearDictionary != null ) 116 { 117 if( appearanceName == null ) 118 { 119 appearanceName = "default"; 120 } 121 Map appearanceMap = appearDictionary.getNormalAppearance(); 122 PDAppearanceStream appearance = 123 (PDAppearanceStream)appearanceMap.get( appearanceName ); 124 if( appearance != null ) 125 { 126 g.translate( (int)rect.getLowerLeftX(), (int)-rect.getLowerLeftY() ); 127 processSubStream( page, appearance.getResources(), appearance.getStream() ); 128 g.translate( (int)-rect.getLowerLeftX(), (int)+rect.getLowerLeftY() ); 129 } 130 } 131 } 132 133 } 134 135 /** 136 * You should override this method if you want to perform an action when a 137 * text is being processed. 138 * 139 * @param text The text to process 140 */ 141 protected void processTextPosition( TextPosition text ) 142 { 143 //should use colorspaces for the font color but for now assume that 144 //the font color is black 145 try 146 { 147 if( this.getGraphicsState().getTextState().getRenderingMode() == PDTextState.RENDERING_MODE_FILL_TEXT ) 148 { 149 graphics.setColor( this.getGraphicsState().getNonStrokingColor().getJavaColor() ); 150 } 151 else if( this.getGraphicsState().getTextState().getRenderingMode() 152 == PDTextState.RENDERING_MODE_STROKE_TEXT ) 153 { 154 graphics.setColor( this.getGraphicsState().getStrokingColor().getJavaColor() ); 155 } 156 else 157 { 158 // TODO : need to implement.... 159 log.warn("Unsupported RenderingMode " 160 + this.getGraphicsState().getTextState().getRenderingMode() 161 + " in PageDrawer.processTextPosition()." 162 + " Using RenderingMode " 163 + PDTextState.RENDERING_MODE_FILL_TEXT 164 + " instead"); 165 graphics.setColor( this.getGraphicsState().getNonStrokingColor().getJavaColor() ); 166 } 167 PDFont font = text.getFont(); 168 169 Matrix textPos = text.getTextPos().copy(); 170 float x = textPos.getXPosition(); 171 // the 0,0-reference has to be moved from the lower left (PDF) to the upper left (AWT-graphics) 172 float y = pageSize.height - textPos.getYPosition(); 173 // Set translation to 0,0. We only need the scaling and shearing 174 textPos.setValue(2, 0, 0); 175 textPos.setValue(2, 1, 0); 176 // because of the moved 0,0-reference, we have to shear in the opposite direction 177 textPos.setValue(0, 1, (-1)*textPos.getValue(0, 1)); 178 textPos.setValue(1, 0, (-1)*textPos.getValue(1, 0)); 179 AffineTransform at = textPos.createAffineTransform(); 180 PDMatrix fontMatrix = font.getFontMatrix(); 181 at.scale(fontMatrix.getValue(0, 0) * 1000f, fontMatrix.getValue(1, 0) * 1000f); 182 graphics.setClip(getGraphicsState().getCurrentClippingPath()); 183 font.drawString( text.getCharacter(), graphics, text.getFontSize(), at, x, y ); 184 } 185 catch( IOException io ) 186 { 187 io.printStackTrace(); 188 } 189 } 190 191 /** 192 * Get the graphics that we are currently drawing on. 193 * 194 * @return The graphics we are drawing on. 195 */ 196 public Graphics2D getGraphics() 197 { 198 return graphics; 199 } 200 201 /** 202 * Get the page that is currently being drawn. 203 * 204 * @return The page that is being drawn. 205 */ 206 public PDPage getPage() 207 { 208 return page; 209 } 210 211 /** 212 * Get the size of the page that is currently being drawn. 213 * 214 * @return The size of the page that is being drawn. 215 */ 216 public Dimension getPageSize() 217 { 218 return pageSize; 219 } 220 221 /** 222 * Fix the y coordinate. 223 * 224 * @param y The y coordinate. 225 * @return The updated y coordinate. 226 */ 227 public double fixY( double y ) 228 { 229 return pageSize.getHeight() - y; 230 } 231 232 /** 233 * Get the current line path to be drawn. 234 * 235 * @return The current line path to be drawn. 236 */ 237 public GeneralPath getLinePath() 238 { 239 return linePath; 240 } 241 242 /** 243 * Set the line path to draw. 244 * 245 * @param newLinePath Set the line path to draw. 246 */ 247 public void setLinePath(GeneralPath newLinePath) 248 { 249 if (linePath == null || linePath.getCurrentPoint() == null) 250 { 251 linePath = newLinePath; 252 } 253 else 254 { 255 linePath.append(newLinePath, false); 256 } 257 } 258 259 260 /** 261 * Fill the path. 262 * 263 * @param windingRule The winding rule this path will use. 264 * 265 * @throws IOException If there is an IO error while filling the path. 266 */ 267 public void fillPath(int windingRule) throws IOException 268 { 269 graphics.setColor( getGraphicsState().getNonStrokingColor().getJavaColor() ); 270 getLinePath().setWindingRule(windingRule); 271 graphics.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF ); 272 graphics.setClip(getGraphicsState().getCurrentClippingPath()); 273 graphics.fill( getLinePath() ); 274 getLinePath().reset(); 275 } 276 277 278 /** 279 * This will set the current stroke. 280 * 281 * @param newStroke The current stroke. 282 * 283 */ 284 public void setStroke(BasicStroke newStroke) 285 { 286 getGraphics().setStroke( newStroke ); 287 } 288 289 /** 290 * Stroke the path. 291 * 292 * @throws IOException If there is an IO error while stroking the path. 293 */ 294 public void strokePath() throws IOException 295 { 296 graphics.setColor( getGraphicsState().getStrokingColor().getJavaColor() ); 297 graphics.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF ); 298 graphics.setClip(getGraphicsState().getCurrentClippingPath()); 299 GeneralPath path = getLinePath(); 300 graphics.draw( path ); 301 path.reset(); 302 } 303 304 /** 305 * Called when the color changed. 306 * @param bStroking true for the stroking color, false for the non-stroking color 307 * @throws IOException if an I/O error occurs 308 */ 309 public void colorChanged(boolean bStroking) throws IOException 310 { 311 //logger().info("changing " + (bStroking ? "" : "non") + "stroking color"); 312 } 313 314 //This code generalizes the code Jim Lynch wrote for AppendRectangleToPath 315 /** 316 * use the current transformation matrix to transform a single point. 317 * @param x x-coordinate of the point to be transform 318 * @param y y-coordinate of the point to be transform 319 * @return the transformed coordinates as Point2D.Double 320 */ 321 public java.awt.geom.Point2D.Double transformedPoint(double x, double y) 322 { 323 double[] position = {x,y}; 324 getGraphicsState().getCurrentTransformationMatrix().createAffineTransform().transform( 325 position, 0, position, 0, 1); 326 position[1] = fixY(position[1]); 327 return new Point2D.Double(position[0],position[1]); 328 } 329 330 /** 331 * Set the clipping Path. 332 * 333 * @param windingRule The winding rule this path will use. 334 * 335 */ 336 public void setClippingPath(int windingRule) 337 { 338 PDGraphicsState graphicsState = getGraphicsState(); 339 GeneralPath clippingPath = (GeneralPath)getLinePath().clone(); 340 clippingPath.setWindingRule(windingRule); 341 // If there is already set a clipping path, we have to intersect the new with the existing one 342 if (graphicsState.getCurrentClippingPath() != null) 343 { 344 Area currentArea = new Area(getGraphicsState().getCurrentClippingPath()); 345 Area newArea = new Area(clippingPath); 346 currentArea.intersect(newArea); 347 graphicsState.setCurrentClippingPath(currentArea); 348 } 349 else 350 { 351 graphicsState.setCurrentClippingPath(clippingPath); 352 } 353 getLinePath().reset(); 354 } 355 356 /** 357 * Fill with Shading. Called by SHFill operator. 358 * 359 * @param ShadingName The name of the Shading Dictionary to use for this fill instruction. 360 * 361 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 362 */ 363 public void SHFill(COSName ShadingName) throws IOException 364 { 365 PDShading Shading =FindShadingDictionary(ShadingName); 366 log.info("Shading = " + Shading.toString()); 367 368 switch (Shading.getShadingType()){ 369 case 1: 370 SHFill_Function(Shading); 371 break; 372 case 2: 373 SHFill_Axial(Shading); 374 break; 375 case 3: 376 SHFill_Radial(Shading); 377 break; 378 case 4: 379 SHFill_FreeGourad(Shading); 380 break; 381 case 5: 382 SHFill_LatticeGourad(Shading); 383 break; 384 case 6: 385 SHFill_CoonsPatch(Shading); 386 break; 387 case 7: 388 SHFill_TensorPatch(Shading); 389 break; 390 391 default: 392 throw new IOException("Invalid ShadingType " + Shading.getShadingType() + " for Shading " + ShadingName); 393 } 394 } 395 396 /** 397 * Find the appropriate Shading Dictionary. This is its own private function as it is really not appropriate to override when deriving from PageDrawer. 398 * 399 * @param ShadingName The name of the Shading Dictionary to use for this fill instruction. 400 * 401 * @returns The PDShading object 402 * @throws IOException If there is an IO error while attempting to find the appropriate PDShading object. 403 */ 404 private PDShading FindShadingDictionary(COSName ShadingName) throws IOException 405 { 406 407 PDResources resources = (PDResources)page.getResources(); 408 409 COSDictionary AllShadings = (COSDictionary)(resources.getCOSDictionary().getDictionaryObject(COSName.SHADING)); 410 411 PDShading Shading = new PDShading(ShadingName, (COSDictionary)(AllShadings.getDictionaryObject(ShadingName))); 412 413 return Shading; 414 415 } 416 417 /** 418 * Fill with a Function-based gradient / shading. 419 * If extending the class, override this and its siblings, not the public SHFill method. 420 * 421 * @param Shading The Shading Dictionary to use for this fill instruction. 422 * 423 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 424 */ 425 protected void SHFill_Function(PDShading Shading) throws IOException 426 { 427 throw new IOException("Not Implemented"); 428 } 429 430 /** 431 * Fill with an Axial Shading. 432 * If extending the class, override this and its siblings, not the public SHFill method. 433 * 434 * @param Shading The Shading Dictionary to use for this fill instruction. 435 * 436 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 437 */ 438 protected void SHFill_Axial(PDShading Shading) throws IOException 439 { 440 throw new IOException("Not Implemented"); 441 442 } 443 444 /** 445 * Fill with a Radial gradient / shading. 446 * If extending the class, override this and its siblings, not the public SHFill method. 447 * 448 * @param Shading The Shading Dictionary to use for this fill instruction. 449 * 450 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 451 */ 452 protected void SHFill_Radial(PDShading Shading) throws IOException 453 { 454 throw new IOException("Not Implemented"); 455 } 456 457 /** 458 * Fill with a Free-form Gourad-shaded triangle mesh. 459 * If extending the class, override this and its siblings, not the public SHFill method. 460 * 461 * @param Shading The Shading Dictionary to use for this fill instruction. 462 * 463 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 464 */ 465 protected void SHFill_FreeGourad(PDShading Shading) throws IOException 466 { 467 throw new IOException("Not Implemented"); 468 } 469 470 /** 471 * Fill with a Lattice-form Gourad-shaded triangle mesh. 472 * If extending the class, override this and its siblings, not the public SHFill method. 473 * 474 * @param Shading The Shading Dictionary to use for this fill instruction. 475 * 476 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 477 */ 478 protected void SHFill_LatticeGourad(PDShading Shading) throws IOException 479 { 480 throw new IOException("Not Implemented"); 481 } 482 483 /** 484 * Fill with a Coons patch mesh 485 * If extending the class, override this and its siblings, not the public SHFill method. 486 * 487 * @param Shading The Shading Dictionary to use for this fill instruction. 488 * 489 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 490 */ 491 protected void SHFill_CoonsPatch(PDShading Shading) throws IOException 492 { 493 throw new IOException("Not Implemented"); 494 } 495 496 /** 497 * Fill with a Tensor-product patch mesh. 498 * If extending the class, override this and its siblings, not the public SHFill method. 499 * 500 * @param Shading The Shading Dictionary to use for this fill instruction. 501 * 502 * @throws IOException If there is an IO error while shade-filling the path/clipping area. 503 */ 504 protected void SHFill_TensorPatch(PDShading Shading) throws IOException 505 { 506 throw new IOException("Not Implemented"); 507 } 508 }