/* ===================================================================== * OrsonPDF : a fast, light-weight PDF library for the Java(tm) platform * ===================================================================== * * (C)opyright 2013-2015, by Object Refinery Limited. All rights reserved. * * Project Info: http://www.object-refinery.com/orsonpdf/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * Orson PDF home page: * * http://www.object-refinery.com/orsonpdf/index.html * */ package com.orsonpdf; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Image; import java.awt.MultipleGradientPaint; import java.awt.RadialGradientPaint; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.orsonpdf.Pattern.ShadingPattern; import com.orsonpdf.filter.FlateFilter; import com.orsonpdf.shading.AxialShading; import com.orsonpdf.shading.RadialShading; import com.orsonpdf.shading.Shading; import com.orsonpdf.util.Args; import com.orsonpdf.util.GradientPaintKey; import com.orsonpdf.util.RadialGradientPaintKey; /** * Represents a page in a {@link PDFDocument}. Our objective is to be able * to write to the page using the {@link PDFGraphics2D} class (see the * {@link #getGraphics2D()} method). */ public class Page extends PDFObject { /** The pages of the document. */ private Pages parent; /** The page bounds. */ private Rectangle2D bounds; /** The page contents. */ private GraphicsStream contents; /** The Graphics2D for writing to the page contents. */ private PDFGraphics2D graphics2d; /** * The list of font (names) used on the page. We let the parent take * care of tracking the font objects. */ private List<String> fontsOnPage; /** * A map between gradient paints and the names used to define the * associated pattern in the page resources. */ private Map<GradientPaintKey, String> gradientPaintsOnPage; private Map<RadialGradientPaintKey, String> radialGradientPaintsOnPage; /** The pattern dictionary for this page. */ private Dictionary patterns; /** The ExtGState dictionary for the page. */ private Dictionary graphicsStates; /** * The transform between Page and Java2D coordinates, used in Shading * patterns. */ private AffineTransform j2DTransform; private Dictionary xObjects = new Dictionary(); /** * Creates a new page. * * @param number the PDF object number. * @param generation the PDF object generation number. * @param parent the parent (manages the pages in the {@code PDFDocument}). * @param bounds the page bounds ({@code null} not permitted). */ Page(int number, int generation, Pages parent, Rectangle2D bounds) { this(number, generation, parent, bounds, true); } /** * Creates a new page. * * @param number the PDF object number. * @param generation the PDF object generation number. * @param parent the parent (manages the pages in the {@code PDFDocument}). * @param bounds the page bounds ({@code null} not permitted). * @param filter a flag that controls whether or not the graphics stream * for the page has a FlateFilter applied. * * @since 1.4 */ Page(int number, int generation, Pages parent, Rectangle2D bounds, boolean filter) { super(number, generation); Args.nullNotPermitted(bounds, "bounds"); this.parent = parent; this.bounds = (Rectangle2D) bounds.clone(); this.fontsOnPage = new ArrayList<String>(); int n = this.parent.getDocument().getNextNumber(); this.contents = new GraphicsStream(n, this); if (filter) { this.contents.addFilter(new FlateFilter()); } this.gradientPaintsOnPage = new HashMap<GradientPaintKey, String>(); this.radialGradientPaintsOnPage = new HashMap<RadialGradientPaintKey, String>(); this.patterns = new Dictionary(); this.graphicsStates = new Dictionary(); this.j2DTransform = AffineTransform.getTranslateInstance(0.0, bounds.getHeight()); this.j2DTransform.concatenate(AffineTransform.getScaleInstance(1.0, -1.0)); } /** * Returns a new rectangle containing the bounds for this page (as supplied * to the constructor). * * @return The page bounds. */ public Rectangle2D getBounds() { return (Rectangle2D) this.bounds.clone(); } /** * Returns the {@code PDFObject} that represents the page content. * * @return The {@code PDFObject} that represents the page content. */ public PDFObject getContents() { return this.contents; } /** * Returns the {@link PDFGraphics2D} instance for drawing to the page. * * @return The {@code PDFGraphics2D} instance for drawing to the page. */ public PDFGraphics2D getGraphics2D() { if (this.graphics2d == null) { this.graphics2d = new PDFGraphics2D(this.contents, (int) this.bounds.getWidth(), (int) this.bounds.getHeight()); } return this.graphics2d; } /** * Finds the font reference corresponding to the given Java2D font, * creating a new one if there isn't one already. * * @param font the AWT font. * * @return The font reference. */ String findOrCreateFontReference(Font font) { String ref = this.parent.findOrCreateFontReference(font); if (!this.fontsOnPage.contains(ref)) { this.fontsOnPage.add(ref); } return ref; } private Dictionary createFontDictionary() { Dictionary d = new Dictionary(); for (String name : this.fontsOnPage) { PDFFont f = this.parent.getFont(name); d.put(name, f.getReference()); } return d; } /** * Returns the name of the pattern for the specified {@code GradientPaint}, * reusing an existing pattern if possible, otherwise creating a new * pattern if necessary. * * @param gp the gradient ({@code null} not permitted). * * @return The pattern name. */ String findOrCreatePattern(GradientPaint gp) { GradientPaintKey key = new GradientPaintKey(gp); String patternName = this.gradientPaintsOnPage.get(key); if (patternName == null) { PDFDocument doc = this.parent.getDocument(); Function f = new ExponentialInterpolationFunction( doc.getNextNumber(), gp.getColor1().getRGBColorComponents(null), gp.getColor2().getRGBColorComponents(null)); doc.addObject(f); double[] coords = new double[4]; coords[0] = gp.getPoint1().getX(); coords[1] = gp.getPoint1().getY(); coords[2] = gp.getPoint2().getX(); coords[3] = gp.getPoint2().getY(); Shading s = new AxialShading(doc.getNextNumber(), coords, f); doc.addObject(s); Pattern p = new ShadingPattern(doc.getNextNumber(), s, this.j2DTransform); doc.addObject(p); patternName = "/P" + (this.patterns.size() + 1); this.patterns.put(patternName, p); this.gradientPaintsOnPage.put(key, patternName); } return patternName; } /** * Returns the name of the pattern for the specified * {@code RadialGradientPaint}, reusing an existing pattern if * possible, otherwise creating a new pattern if necessary. * * @param gp the gradient ({@code null} not permitted). * * @return The pattern name. */ String findOrCreatePattern(RadialGradientPaint gp) { RadialGradientPaintKey key = new RadialGradientPaintKey(gp); String patternName = this.radialGradientPaintsOnPage.get(key); if (patternName == null) { PDFDocument doc = this.parent.getDocument(); Function f = createFunctionForMultipleGradient(gp); doc.addObject(f); double[] coords = new double[6]; coords[0] = gp.getFocusPoint().getX(); coords[1] = gp.getFocusPoint().getY(); coords[2] = 0.0; coords[3] = gp.getCenterPoint().getX(); coords[4] = gp.getCenterPoint().getY(); coords[5] = gp.getRadius(); Shading s = new RadialShading(doc.getNextNumber(), coords, f); doc.addObject(s); Pattern p = new ShadingPattern(doc.getNextNumber(), s, this.j2DTransform); doc.addObject(p); patternName = "/P" + (this.patterns.size() + 1); this.patterns.put(patternName, p); this.radialGradientPaintsOnPage.put(key, patternName); } return patternName; } private Function createFunctionForMultipleGradient( MultipleGradientPaint mgp) { PDFDocument doc = this.parent.getDocument(); if (mgp.getColors().length == 2) { Function f = new ExponentialInterpolationFunction( doc.getNextNumber(), mgp.getColors()[0].getRGBColorComponents(null), mgp.getColors()[1].getRGBColorComponents(null)); return f; } else { int count = mgp.getColors().length - 1; Function[] functions = new Function[count]; float[] fbounds = new float[count - 1]; float[] encode = new float[count * 2]; for (int i = 0; i < count; i++) { // create a linear function for each pair of colors functions[i] = new ExponentialInterpolationFunction( doc.getNextNumber(), mgp.getColors()[i].getRGBColorComponents(null), mgp.getColors()[i + 1].getRGBColorComponents(null)); doc.addObject(functions[i]); if (i < count - 1) { fbounds[i] = mgp.getFractions()[i + 1]; } encode[i * 2] = 0; encode[i * 2 + 1] = 1; } return new StitchingFunction(doc.getNextNumber(), functions, fbounds, encode); } } private Map<Integer, String> alphaDictionaries = new HashMap<Integer, String>(); /** * Returns the name of the Graphics State Dictionary that can be used * for the specified alpha value - if there is no existing dictionary * then a new one is created. * * @param alpha the alpha value in the range 0 to 255. * * @return The graphics state dictionary reference. */ String findOrCreateGSDictionary(int alpha) { Integer key = Integer.valueOf(alpha); float alphaValue = alpha / 255f; String name = this.alphaDictionaries.get(key); if (name == null) { PDFDocument pdfDoc = this.parent.getDocument(); GraphicsStateDictionary gsd = new GraphicsStateDictionary( pdfDoc.getNextNumber()); gsd.setNonStrokeAlpha(alphaValue); gsd.setStrokeAlpha(alphaValue); pdfDoc.addObject(gsd); name = "/GS" + (this.graphicsStates.size() + 1); this.graphicsStates.put(name, gsd); this.alphaDictionaries.put(key, name); } return name; } /** * Adds a soft mask image to the page. This is called from the * {@link #addImage(java.awt.Image)} method to support image transparency. * * @param img the image ({@code null} not permitted). * * @return The soft mask image reference. */ String addSoftMaskImage(Image img) { Args.nullNotPermitted(img, "img"); PDFDocument pdfDoc = this.parent.getDocument(); PDFSoftMaskImage softMaskImage = new PDFSoftMaskImage( pdfDoc.getNextNumber(), img); softMaskImage.addFilter(new FlateFilter()); pdfDoc.addObject(softMaskImage); String reference = "/Image" + this.xObjects.size(); this.xObjects.put(reference, softMaskImage); return softMaskImage.getReference(); } /** * Adds an image to the page. This creates the required PDF object, * as well as adding a reference in the {@code xObjects} resources. * You should not call this method directly, it exists for the use of the * {@link PDFGraphics2D#drawImage(java.awt.Image, int, int, int, int, java.awt.image.ImageObserver)} * method. * * @param img the image ({@code null} not permitted). * * @return The image reference name. */ String addImage(Image img, boolean addSoftMaskImage) { Args.nullNotPermitted(img, "img"); PDFDocument pdfDoc = this.parent.getDocument(); String softMaskImageRef = null; if (addSoftMaskImage) { softMaskImageRef = addSoftMaskImage(img); } PDFImage image = new PDFImage(pdfDoc.getNextNumber(), img, softMaskImageRef); image.addFilter(new FlateFilter()); pdfDoc.addObject(image); String reference = "/Image" + this.xObjects.size(); this.xObjects.put(reference, image); return reference; } @Override public byte[] getObjectBytes() { return createDictionary().toPDFBytes(); } private Dictionary createDictionary() { Dictionary dictionary = new Dictionary("/Page"); dictionary.put("/Parent", this.parent); dictionary.put("/MediaBox", this.bounds); dictionary.put("/Contents", this.contents); Dictionary resources = new Dictionary(); resources.put("/ProcSet", "[/PDF /Text /ImageB /ImageC /ImageI]"); if (!this.xObjects.isEmpty()) { resources.put("/XObject", this.xObjects); } if (!this.fontsOnPage.isEmpty()) { resources.put("/Font", createFontDictionary()); } if (!this.patterns.isEmpty()) { resources.put("/Pattern", this.patterns); } if (!this.graphicsStates.isEmpty()) { resources.put("/ExtGState", this.graphicsStates); } dictionary.put("/Resources", resources); return dictionary; } }