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