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}