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