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