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}