    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.geom.Rectangle2D;
    035import java.io.ByteArrayOutputStream;
    036import java.io.File;
    037import java.io.FileNotFoundException;
    038import java.io.FileOutputStream;
    039import java.io.IOException;
    040import java.io.UnsupportedEncodingException;
    041import java.util.ArrayList;
    042import java.util.Date;
    043import java.util.List;
    044import java.util.logging.Level;
    045import java.util.logging.Logger;
    046import org.jfree.pdf.dictionary.Dictionary;
    047import org.jfree.pdf.dictionary.DictionaryObject;
    048import org.jfree.pdf.internal.PDFFont;
    049import org.jfree.pdf.internal.Pages;
    050import org.jfree.pdf.internal.PDFObject;
    051import org.jfree.pdf.util.Args;
    052import org.jfree.pdf.util.PDFUtils;
    053
    054/**
    055 * Represents a PDF document.  The focus of this implementation is to
    056 * allow the use of the {@link PDFGraphics2D} class to generate PDF content,
    057 * typically in the following manner:
    058 * <p>
    059 * <code>PDFDocument pdfDoc = new PDFDocument();<br></code>
    060 * <code>Page page = pdfDoc.createPage(new Rectangle(612, 468));<br></code>
    061 * <code>PDFGraphics2D g2 = page.getGraphics2D();<br></code>
    062 * <code>g2.setPaint(Color.RED);<br></code>
    063 * <code>g2.draw(new Rectangle(10, 10, 40, 50));<br></code>
    064 * <code>pdfDoc.writeToFile(new File("demo.pdf"));<br></code>
    065 * <p>
    066 * The implementation is light-weight and works very well alongside packages
    067 * such as <b>JFreeChart</b> and <b>Orson Charts</b>.
    068 */
    069public class PDFDocument {
    070
    071    private static final Logger LOGGER = Logger.getLogger(
    072            PDFDocument.class.getName());
    073
    074    /** Producer string. */
    075    private static final String PRODUCER = "JFreePDF 2.0";
    076
    077    /** The document catalog. */
    078    private DictionaryObject catalog;
    079
    080    /** The outlines (placeholder, outline support is not implemented). */
    081    private DictionaryObject outlines;
    082
    083    /** Document info. */
    084    private DictionaryObject info;
    085
    086    /** The document title (can be null). */
    087    private String title;
    088
    089    /** The author of the document (can be null). */
    090    private String author;
    091
    092    /** The pages of the document. */
    093    private Pages pages;
    094
    095    /** A list of other objects added to the document. */
    096    private List<PDFObject> otherObjects;
    097
    098    /** The next PDF object number in the document. */
    099    private int nextNumber = 1;
    100
    101    /**
    102     * A flag that is used to indicate that we are in DEBUG mode.  In this
    103     * mode, the graphics stream for a page does not have a filter applied, so
    104     * the output can be read in a text editor.
    105     */
    106    private boolean debug;
    107
    108    /**
    109     * Creates a new {@code PDFDocument}, initially with no content.
    110     */
    111    public PDFDocument() {
    112        this.catalog = new DictionaryObject(this.nextNumber++, "/Catalog");
    113        this.outlines = new DictionaryObject(this.nextNumber++, "/Outlines");
    114        this.info = new DictionaryObject(this.nextNumber++, "/Info");
    115        StringBuilder producer = new StringBuilder("(").append(PRODUCER);
    116        producer.append(")");
    117        this.info.put("Producer", producer.toString());
    118        Date now = new Date();
    119        String creationDateStr = "(" + PDFUtils.toDateFormat(now) + ")";
    120        this.info.put("CreationDate", creationDateStr);
    121        this.info.put("ModDate", creationDateStr);
    122        this.outlines.put("Count", 0);
    123        this.catalog.put("Outlines", this.outlines);
    124        this.pages = new Pages(this.nextNumber++, 0, this);
    125        this.catalog.put("Pages", this.pages);
    126        this.otherObjects = new ArrayList<>();
    127    }
    128
    129    /**
    130     * Returns the title for the document.  The default value is {@code null}.
    131     *
    132     * @return The title for the document (possibly {@code null}).
    133     */
    134    public String getTitle() {
    135        return this.title;
    136    }
    137
    138    /**
    139     * Sets the title for the document.
    140     *
    141     * @param title  the title ({@code null} permitted).
    142     */
    143    public void setTitle(String title) {
    144        this.title = title;
    145        if (title != null) {
    146            this.info.put("Title", "(" + title + ")");
    147        } else {
    148            this.info.remove("Title");
    149        }
    150    }
    151
    152    /**
    153     * Returns the author for the document.  The default value is {@code null}.
    154     *
    155     * @return The author for the document (possibly {@code null}).
    156     */
    157    public String getAuthor() {
    158        return this.author;
    159    }
    160
    161    /**
    162     * Sets the author for the document.
    163     *
    164     * @param author  the author ({@code null} permitted).
    165     */
    166    public void setAuthor(String author) {
    167        this.author = author;
    168        if (author != null) {
    169            this.info.put("Author", "(" + this.author + ")");
    170        } else {
    171            this.info.remove("Author");
    172        }
    173    }
    174
    175    /**
    176     * Returns the debug mode flag that controls whether or not the output
    177     * stream is filtered.
    178     *
    179     * @return The debug flag.
    180     *
    181     * @since 1.4
    182     */
    183    public boolean isDebugMode() {
    184        return this.debug;
    185    }
    186
    187    /**
    188     * Sets the debug MODE flag (this needs to be set before any call to
    189     * {@link #createPage(java.awt.geom.Rectangle2D)}).
    190     *
    191     * @param debug  the new flag value.
    192     *
    193     * @since 1.4
    194     */
    195    public void setDebugMode(boolean debug) {
    196        this.debug = debug;
    197    }
    198
    199    /**
    200     * Creates a new {@code Page}, adds it to the document, and returns
    201     * a reference to the {@code Page}.
    202     *
    203     * @param bounds  the page bounds ({@code null} not permitted).
    204     *
    205     * @return The new page.
    206     */
    207    public Page createPage(Rectangle2D bounds) {
    208        Page page = new Page(this.nextNumber++, 0, this.pages, bounds,
    209                !this.debug);
    210        this.pages.add(page);
    211        return page;
    212    }
    213
    214    /**
    215     * Adds an object to the document.
    216     *
    217     * @param object  the object ({@code null} not permitted).
    218     */
    219    void addObject(PDFObject object) {
    220        Args.nullNotPermitted(object, "object");
    221        this.otherObjects.add(object);
    222    }
    223
    224    /**
    225     * Returns a new PDF object number and increments the internal counter
    226     * for the next PDF object number.  This method is used to ensure that
    227     * all objects in the document are assigned a unique number.
    228     *
    229     * @return A new PDF object number.
    230     */
    231    public int getNextNumber() {
    232        int result = this.nextNumber;
    233        this.nextNumber++;
    234        return result;
    235    }
    236
    237    /**
    238     * Returns a byte array containing the encoding of this PDF document.
    239     *
    240     * @return A byte array containing the encoding of this PDF document.
    241     */
    242    public byte[] getPDFBytes() {
    243        int[] xref = new int[this.nextNumber];
    244        ByteArrayOutputStream bos = new ByteArrayOutputStream();
    245        try {
    246            bos.write(toBytes("%PDF-1.4\n"));
    247            bos.write(new byte[] { (byte) 37, (byte) 128, (byte) 129,
    248                (byte) 130, (byte) 131, (byte) 10});
    249            xref[this.catalog.getNumber() - 1] = bos.size();  // offset to catalog
    250            bos.write(this.catalog.toPDFBytes());
    251            xref[this.outlines.getNumber() - 1] = bos.size();  // offset to outlines
    252            bos.write(this.outlines.toPDFBytes());
    253            xref[this.info.getNumber() - 1] = bos.size();  // offset to info
    254            bos.write(this.info.toPDFBytes());
    255            xref[this.pages.getNumber() - 1] = bos.size();  // offset to pages
    256            bos.write(this.pages.toPDFBytes());
    257            for (Page page : this.pages.getPages()) {
    258                xref[page.getNumber() - 1] = bos.size();
    259                bos.write(page.toPDFBytes());
    260                PDFObject contents = page.getContents();
    261                xref[contents.getNumber() - 1] = bos.size();
    262                bos.write(contents.toPDFBytes());
    263            }
    264            for (PDFFont font: this.pages.getFonts()) {
    265                xref[font.getNumber() - 1] = bos.size();
    266                bos.write(font.toPDFBytes());
    267            }
    268            for (PDFObject object: this.otherObjects) {
    269                xref[object.getNumber() - 1] = bos.size();
    270                bos.write(object.toPDFBytes());
    271            }
    272            xref[xref.length - 1] = bos.size();
    273            // write the xref table
    274            bos.write(toBytes("xref\n"));
    275            bos.write(toBytes("0 " + String.valueOf(this.nextNumber)
    276                    + "\n"));
    277            bos.write(toBytes("0000000000 65535 f \n"));
    278            for (int i = 0; i < this.nextNumber - 1; i++) {
    279                String offset = String.valueOf(xref[i]);
    280                int len = offset.length();
    281                String offset10 = "0000000000".substring(len) + offset;
    282                bos.write(toBytes(offset10 + " 00000 n \n"));
    283            }
    284
    285            // write the trailer
    286            bos.write(toBytes("trailer\n"));
    287            Dictionary trailer = new Dictionary();
    288            trailer.put("/Size", this.nextNumber);
    289            trailer.put("/Root", this.catalog);
    290            trailer.put("/Info", this.info);
    291            bos.write(trailer.toPDFBytes());
    292            bos.write(toBytes("startxref\n"));
    293            bos.write(toBytes(String.valueOf(xref[this.nextNumber - 1])
    294                    + "\n"));
    295            bos.write(toBytes("%%EOF"));
    296        } catch (IOException ex) {
    297            throw new RuntimeException(ex);
    298        }
    299        return bos.toByteArray();
    300    }
    301
    302    /**
    303     * Writes the PDF document to a file.  This is not a robust method, it
    304     * exists mainly for the demo output.
    305     *
    306     * @param f  the file.
    307     */
    308    public void writeToFile(File f) {
    309        FileOutputStream fos = null;
    310        try {
    311            fos = new FileOutputStream(f);
    312            fos.write(getPDFBytes());
    313        } catch (FileNotFoundException ex) {
    314            LOGGER.log(Level.SEVERE, null, ex);
    315        } catch (IOException ex) {
    316            LOGGER.log(Level.SEVERE, null, ex);
    317        } finally {
    318            try {
    319                if (fos != null) {
    320                    fos.close();
    321                }
    322            } catch (IOException ex) {
    323                LOGGER.log(Level.SEVERE, null, ex);
    324            }
    325        }
    326    }
    327
    328    /**
    329     * A utility method to convert a string to US-ASCII byte format.
    330     *
    331     * @param s  the string.
    332     *
    333     * @return The corresponding byte array.
    334     */
    335    private byte[] toBytes(String s) {
    336        byte[] result = null;
    337        try {
    338            result = s.getBytes("US-ASCII");
    339        } catch (UnsupportedEncodingException ex) {
    340            throw new RuntimeException(ex);
    341        }
    342        return result;
    343    }
    344
    345}