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