    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