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}