001/* ===================================================================== 002 * JFreePDF : a fast, light-weight PDF library for the Java(tm) platform 003 * ===================================================================== 004 * 005 * (C)opyright 2013-2020, by Object Refinery Limited. All rights reserved. 006 * 007 * Project Info: http://www.object-refinery.com/orsonpdf/index.html 008 * 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as published by 011 * the Free Software Foundation, either version 3 of the License, or 012 * (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public License 020 * along with this program. If not, see <http://www.gnu.org/licenses/>. 021 * 022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 023 * Other names may be trademarks of their respective owners.] 024 * 025 * If you do not wish to be bound by the terms of the GPL, an alternative 026 * commercial license can be purchased. For details, please see visit the 027 * Orson PDF home page: 028 * 029 * http://www.object-refinery.com/orsonpdf/index.html 030 * 031 */ 032 033package org.jfree.pdf; 034 035import java.awt.Font; 036import java.awt.GradientPaint; 037import java.awt.Image; 038import java.awt.MultipleGradientPaint; 039import java.awt.RadialGradientPaint; 040import java.awt.geom.AffineTransform; 041import java.awt.geom.Rectangle2D; 042import java.util.ArrayList; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Map; 046import org.jfree.pdf.internal.Pattern.ShadingPattern; 047import org.jfree.pdf.dictionary.Dictionary; 048import org.jfree.pdf.dictionary.GraphicsStateDictionary; 049import org.jfree.pdf.filter.FlateFilter; 050import org.jfree.pdf.function.ExponentialInterpolationFunction; 051import org.jfree.pdf.function.Function; 052import org.jfree.pdf.function.StitchingFunction; 053import org.jfree.pdf.internal.PDFFont; 054import org.jfree.pdf.internal.Pages; 055import org.jfree.pdf.internal.PDFObject; 056import org.jfree.pdf.internal.Pattern; 057import org.jfree.pdf.shading.AxialShading; 058import org.jfree.pdf.shading.RadialShading; 059import org.jfree.pdf.shading.Shading; 060import org.jfree.pdf.stream.GraphicsStream; 061import org.jfree.pdf.stream.PDFImage; 062import org.jfree.pdf.stream.PDFSoftMaskImage; 063import org.jfree.pdf.util.Args; 064import org.jfree.pdf.util.GradientPaintKey; 065import org.jfree.pdf.util.RadialGradientPaintKey; 066 067/** 068 * Represents a page in a {@link PDFDocument}. Our objective is to be able 069 * to write to the page using the {@link PDFGraphics2D} class (see the 070 * {@link #getGraphics2D()} method). 071 */ 072public class Page extends PDFObject { 073 074 /** The pages of the document. */ 075 private Pages parent; 076 077 /** The page bounds. */ 078 private Rectangle2D bounds; 079 080 /** The page contents. */ 081 private GraphicsStream contents; 082 083 /** The Graphics2D for writing to the page contents. */ 084 private PDFGraphics2D graphics2d; 085 086 /** 087 * The list of font (names) used on the page. We let the parent take 088 * care of tracking the font objects. 089 */ 090 private List<String> fontsOnPage; 091 092 /** 093 * A map between gradient paints and the names used to define the 094 * associated pattern in the page resources. 095 */ 096 private Map<GradientPaintKey, String> gradientPaintsOnPage; 097 098 private Map<RadialGradientPaintKey, String> radialGradientPaintsOnPage; 099 100 /** The pattern dictionary for this page. */ 101 private Dictionary patterns; 102 103 /** The ExtGState dictionary for the page. */ 104 private Dictionary graphicsStates; 105 106 /** 107 * The transform between Page and Java2D coordinates, used in Shading 108 * patterns. 109 */ 110 private AffineTransform j2DTransform; 111 112 private Dictionary xObjects = new Dictionary(); 113 114 /** 115 * Creates a new page. 116 * 117 * @param number the PDF object number. 118 * @param generation the PDF object generation number. 119 * @param parent the parent (manages the pages in the {@code PDFDocument}). 120 * @param bounds the page bounds ({@code null} not permitted). 121 */ 122 Page(int number, int generation, Pages parent, Rectangle2D bounds) { 123 this(number, generation, parent, bounds, true); 124 } 125 126 /** 127 * Creates a new page. 128 * 129 * @param number the PDF object number. 130 * @param generation the PDF object generation number. 131 * @param parent the parent (manages the pages in the {@code PDFDocument}). 132 * @param bounds the page bounds ({@code null} not permitted). 133 * @param filter a flag that controls whether or not the graphics stream 134 * for the page has a FlateFilter applied. 135 * 136 * @since 1.4 137 */ 138 Page(int number, int generation, Pages parent, Rectangle2D bounds, 139 boolean filter) { 140 141 super(number, generation); 142 Args.nullNotPermitted(bounds, "bounds"); 143 this.parent = parent; 144 this.bounds = (Rectangle2D) bounds.clone(); 145 this.fontsOnPage = new ArrayList<>(); 146 int n = this.parent.getDocument().getNextNumber(); 147 this.contents = new GraphicsStream(n, this); 148 if (filter) { 149 this.contents.addFilter(new FlateFilter()); 150 } 151 this.gradientPaintsOnPage = new HashMap<>(); 152 this.radialGradientPaintsOnPage = new HashMap<>(); 153 this.patterns = new Dictionary(); 154 this.graphicsStates = new Dictionary(); 155 156 this.j2DTransform = AffineTransform.getTranslateInstance(0.0, 157 bounds.getHeight()); 158 this.j2DTransform.concatenate(AffineTransform.getScaleInstance(1.0, 159 -1.0)); 160 } 161 162 /** 163 * Returns a new rectangle containing the bounds for this page (as supplied 164 * to the constructor). 165 * 166 * @return The page bounds. 167 */ 168 public Rectangle2D getBounds() { 169 return (Rectangle2D) this.bounds.clone(); 170 } 171 172 /** 173 * Returns the {@code PDFObject} that represents the page content. 174 * 175 * @return The {@code PDFObject} that represents the page content. 176 */ 177 public PDFObject getContents() { 178 return this.contents; 179 } 180 181 /** 182 * Returns the {@link PDFGraphics2D} instance for drawing to the page. 183 * 184 * @return The {@code PDFGraphics2D} instance for drawing to the page. 185 */ 186 public PDFGraphics2D getGraphics2D() { 187 if (this.graphics2d == null) { 188 this.graphics2d = new PDFGraphics2D(this.contents, 189 (int) this.bounds.getWidth(), 190 (int) this.bounds.getHeight()); 191 } 192 return this.graphics2d; 193 } 194 195 /** 196 * Finds the font reference corresponding to the given Java2D font, 197 * creating a new one if there isn't one already. 198 * 199 * @param font the AWT font. 200 * 201 * @return The font reference. 202 */ 203 public String findOrCreateFontReference(Font font) { 204 String ref = this.parent.findOrCreateFontReference(font); 205 if (!this.fontsOnPage.contains(ref)) { 206 this.fontsOnPage.add(ref); 207 } 208 return ref; 209 } 210 211 private Dictionary createFontDictionary() { 212 Dictionary d = new Dictionary(); 213 for (String name : this.fontsOnPage) { 214 PDFFont f = this.parent.getFont(name); 215 d.put(name, f.getReference()); 216 } 217 return d; 218 } 219 220 /** 221 * Returns the name of the pattern for the specified {@code GradientPaint}, 222 * reusing an existing pattern if possible, otherwise creating a new 223 * pattern if necessary. 224 * 225 * @param gp the gradient ({@code null} not permitted). 226 * 227 * @return The pattern name. 228 */ 229 public String findOrCreatePattern(GradientPaint gp) { 230 GradientPaintKey key = new GradientPaintKey(gp); 231 String patternName = this.gradientPaintsOnPage.get(key); 232 if (patternName == null) { 233 PDFDocument doc = this.parent.getDocument(); 234 Function f = new ExponentialInterpolationFunction( 235 doc.getNextNumber(), 236 gp.getColor1().getRGBColorComponents(null), 237 gp.getColor2().getRGBColorComponents(null)); 238 doc.addObject(f); 239 double[] coords = new double[4]; 240 coords[0] = gp.getPoint1().getX(); 241 coords[1] = gp.getPoint1().getY(); 242 coords[2] = gp.getPoint2().getX(); 243 coords[3] = gp.getPoint2().getY(); 244 Shading s = new AxialShading(doc.getNextNumber(), coords, f); 245 doc.addObject(s); 246 Pattern p = new ShadingPattern(doc.getNextNumber(), s, 247 this.j2DTransform); 248 doc.addObject(p); 249 patternName = "/P" + (this.patterns.size() + 1); 250 this.patterns.put(patternName, p); 251 this.gradientPaintsOnPage.put(key, patternName); 252 } 253 return patternName; 254 } 255 256 /** 257 * Returns the name of the pattern for the specified 258 * {@code RadialGradientPaint}, reusing an existing pattern if 259 * possible, otherwise creating a new pattern if necessary. 260 * 261 * @param gp the gradient ({@code null} not permitted). 262 * 263 * @return The pattern name. 264 */ 265 public String findOrCreatePattern(RadialGradientPaint gp) { 266 RadialGradientPaintKey key = new RadialGradientPaintKey(gp); 267 String patternName = this.radialGradientPaintsOnPage.get(key); 268 if (patternName == null) { 269 PDFDocument doc = this.parent.getDocument(); 270 Function f = createFunctionForMultipleGradient(gp); 271 doc.addObject(f); 272 double[] coords = new double[6]; 273 coords[0] = gp.getFocusPoint().getX(); 274 coords[1] = gp.getFocusPoint().getY(); 275 coords[2] = 0.0; 276 coords[3] = gp.getCenterPoint().getX(); 277 coords[4] = gp.getCenterPoint().getY(); 278 coords[5] = gp.getRadius(); 279 Shading s = new RadialShading(doc.getNextNumber(), coords, f); 280 doc.addObject(s); 281 Pattern p = new ShadingPattern(doc.getNextNumber(), s, 282 this.j2DTransform); 283 doc.addObject(p); 284 patternName = "/P" + (this.patterns.size() + 1); 285 this.patterns.put(patternName, p); 286 this.radialGradientPaintsOnPage.put(key, patternName); 287 } 288 return patternName; 289 } 290 291 private Function createFunctionForMultipleGradient( 292 MultipleGradientPaint mgp) { 293 PDFDocument doc = this.parent.getDocument(); 294 295 if (mgp.getColors().length == 2) { 296 Function f = new ExponentialInterpolationFunction( 297 doc.getNextNumber(), 298 mgp.getColors()[0].getRGBColorComponents(null), 299 mgp.getColors()[1].getRGBColorComponents(null)); 300 return f; 301 } else { 302 int count = mgp.getColors().length - 1; 303 Function[] functions = new Function[count]; 304 float[] fbounds = new float[count - 1]; 305 float[] encode = new float[count * 2]; 306 for (int i = 0; i < count; i++) { 307 // create a linear function for each pair of colors 308 functions[i] = new ExponentialInterpolationFunction( 309 doc.getNextNumber(), 310 mgp.getColors()[i].getRGBColorComponents(null), 311 mgp.getColors()[i + 1].getRGBColorComponents(null)); 312 doc.addObject(functions[i]); 313 if (i < count - 1) { 314 fbounds[i] = mgp.getFractions()[i + 1]; 315 } 316 encode[i * 2] = 0; 317 encode[i * 2 + 1] = 1; 318 } 319 return new StitchingFunction(doc.getNextNumber(), functions, 320 fbounds, encode); 321 } 322 } 323 324 private Map<Integer, String> alphaDictionaries = new HashMap<>(); 325 326 /** 327 * Returns the name of the Graphics State Dictionary that can be used 328 * for the specified alpha value - if there is no existing dictionary 329 * then a new one is created. 330 * 331 * @param alpha the alpha value in the range 0 to 255. 332 * 333 * @return The graphics state dictionary reference. 334 */ 335 public String findOrCreateGSDictionary(int alpha) { 336 Integer key = alpha; 337 float alphaValue = alpha / 255f; 338 String name = this.alphaDictionaries.get(key); 339 if (name == null) { 340 PDFDocument pdfDoc = this.parent.getDocument(); 341 GraphicsStateDictionary gsd = new GraphicsStateDictionary( 342 pdfDoc.getNextNumber()); 343 gsd.setNonStrokeAlpha(alphaValue); 344 gsd.setStrokeAlpha(alphaValue); 345 pdfDoc.addObject(gsd); 346 name = "/GS" + (this.graphicsStates.size() + 1); 347 this.graphicsStates.put(name, gsd); 348 this.alphaDictionaries.put(key, name); 349 } 350 return name; 351 } 352 353 /** 354 * Adds a soft mask image to the page. This is called from the 355 * {@link #addImage(java.awt.Image)} method to support image transparency. 356 * 357 * @param img the image ({@code null} not permitted). 358 * 359 * @return The soft mask image reference. 360 */ 361 String addSoftMaskImage(Image img) { 362 Args.nullNotPermitted(img, "img"); 363 PDFDocument pdfDoc = this.parent.getDocument(); 364 PDFSoftMaskImage softMaskImage = new PDFSoftMaskImage( 365 pdfDoc.getNextNumber(), img); 366 softMaskImage.addFilter(new FlateFilter()); 367 pdfDoc.addObject(softMaskImage); 368 String reference = "/Image" + this.xObjects.size(); 369 this.xObjects.put(reference, softMaskImage); 370 return softMaskImage.getReference(); 371 } 372 373 /** 374 * Adds an image to the page.This creates the required PDF object, 375 * as well as adding a reference in the {@code xObjects} resources. 376 * You should not call this method directly, it exists for the use of the 377 * {@link PDFGraphics2D#drawImage(java.awt.Image, int, int, int, int, java.awt.image.ImageObserver)} 378 * method. 379 * 380 * @param img the image ({@code null} not permitted). 381 * @param addSoftMaskImage add as a soft mask image? 382 * 383 * @return The image reference name. 384 */ 385 public String addImage(Image img, boolean addSoftMaskImage) { 386 Args.nullNotPermitted(img, "img"); 387 PDFDocument pdfDoc = this.parent.getDocument(); 388 String softMaskImageRef = null; 389 if (addSoftMaskImage) { 390 softMaskImageRef = addSoftMaskImage(img); 391 } 392 PDFImage image = new PDFImage(pdfDoc.getNextNumber(), img, 393 softMaskImageRef); 394 image.addFilter(new FlateFilter()); 395 pdfDoc.addObject(image); 396 String reference = "/Image" + this.xObjects.size(); 397 this.xObjects.put(reference, image); 398 return reference; 399 } 400 401 @Override 402 public byte[] getObjectBytes() { 403 return createDictionary().toPDFBytes(); 404 } 405 406 private Dictionary createDictionary() { 407 Dictionary dictionary = new Dictionary("/Page"); 408 dictionary.put("/Parent", this.parent); 409 dictionary.put("/MediaBox", this.bounds); 410 dictionary.put("/Contents", this.contents); 411 Dictionary resources = new Dictionary(); 412 resources.put("/ProcSet", "[/PDF /Text /ImageB /ImageC /ImageI]"); 413 if (!this.xObjects.isEmpty()) { 414 resources.put("/XObject", this.xObjects); 415 } 416 if (!this.fontsOnPage.isEmpty()) { 417 resources.put("/Font", createFontDictionary()); 418 } 419 if (!this.patterns.isEmpty()) { 420 resources.put("/Pattern", this.patterns); 421 } 422 if (!this.graphicsStates.isEmpty()) { 423 resources.put("/ExtGState", this.graphicsStates); 424 } 425 dictionary.put("/Resources", resources); 426 return dictionary; 427 } 428 429} 430