/* =====================================================================
* OrsonPDF : a fast, light-weight PDF library for the Java(tm) platform
* =====================================================================
*
* (C)opyright 2013-2015, by Object Refinery Limited. All rights reserved.
*
* Project Info: http://www.object-refinery.com/orsonpdf/index.html
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.]
*
* If you do not wish to be bound by the terms of the GPL, an alternative
* commercial license can be purchased. For details, please see visit the
* Orson PDF home page:
*
* http://www.object-refinery.com/orsonpdf/index.html
*
*/
package com.orsonpdf;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.orsonpdf.util.Args;
/**
* Represents a PDF document. The focus of this implementation is to
* allow the use of the {@link PDFGraphics2D} class to generate PDF content,
* typically in the following manner:
* <p>
* <code>PDFDocument pdfDoc = new PDFDocument();<br></code>
* <code>Page page = pdfDoc.createPage(new Rectangle(612, 468));<br></code>
* <code>PDFGraphics2D g2 = page.getGraphics2D();<br></code>
* <code>g2.setPaint(Color.RED);<br></code>
* <code>g2.draw(new Rectangle(10, 10, 40, 50));<br></code>
* <code>pdfDoc.writeToFile(new File("demo.pdf"));<br></code>
* <p>
* The implementation is light-weight and works very well alongside packages
* such as <b>JFreeChart</b> and <b>Orson Charts</b>.
*/
public class PDFDocument {
private static final Logger LOGGER = Logger.getLogger(
PDFDocument.class.getName());
/** Producer string. */
private static final String PRODUCER = "OrsonPDF 1.7";
/** The document catalog. */
private DictionaryObject catalog;
/** The outlines (placeholder, outline support is not implemented). */
private DictionaryObject outlines;
/** Document info. */
private DictionaryObject info;
/** The document title (can be null). */
private String title;
/** The author of the document (can be null). */
private String author;
/** The pages of the document. */
private Pages pages;
/** A list of other objects added to the document. */
private List<PDFObject> otherObjects;
/** The next PDF object number in the document. */
private int nextNumber = 1;
/**
* A flag that is used to indicate that we are in DEBUG mode. In this
* mode, the graphics stream for a page does not have a filter applied, so
* the output can be read in a text editor.
*/
private boolean debug;
/**
* Creates a new {@code PDFDocument}, initially with no content.
*/
public PDFDocument() {
this.catalog = new DictionaryObject(this.nextNumber++, "/Catalog");
this.outlines = new DictionaryObject(this.nextNumber++, "/Outlines");
this.info = new DictionaryObject(this.nextNumber++, "/Info");
StringBuilder producer = new StringBuilder("(").append(PRODUCER);
producer.append(")");
this.info.put("Producer", producer.toString());
Date now = new Date();
String creationDateStr = "(" + PDFUtils.toDateFormat(now) + ")";
this.info.put("CreationDate", creationDateStr);
this.info.put("ModDate", creationDateStr);
this.outlines.put("Count", Integer.valueOf(0));
this.catalog.put("Outlines", this.outlines);
this.pages = new Pages(this.nextNumber++, 0, this);
this.catalog.put("Pages", this.pages);
this.otherObjects = new ArrayList<PDFObject>();
}
/**
* Returns the title for the document. The default value is {@code null}.
*
* @return The title for the document (possibly {@code null}).
*/
public String getTitle() {
return this.title;
}
/**
* Sets the title for the document.
*
* @param title the title ({@code null} permitted).
*/
public void setTitle(String title) {
this.title = title;
if (title != null) {
this.info.put("Title", "(" + title + ")");
} else {
this.info.remove("Title");
}
}
/**
* Returns the author for the document. The default value is {@code null}.
*
* @return The author for the document (possibly {@code null}).
*/
public String getAuthor() {
return this.author;
}
/**
* Sets the author for the document.
*
* @param author the author ({@code null} permitted).
*/
public void setAuthor(String author) {
this.author = author;
if (author != null) {
this.info.put("Author", "(" + this.author + ")");
} else {
this.info.remove("Author");
}
}
/**
* Returns the debug mode flag that controls whether or not the output
* stream is filtered.
*
* @return The debug flag.
*
* @since 1.4
*/
public boolean isDebugMode() {
return this.debug;
}
/**
* Sets the debug MODE flag (this needs to be set before any call to
* {@link #createPage(java.awt.geom.Rectangle2D)}).
*
* @param debug the new flag value.
*
* @since 1.4
*/
public void setDebugMode(boolean debug) {
this.debug = debug;
}
/**
* Creates a new {@code Page}, adds it to the document, and returns
* a reference to the {@code Page}.
*
* @param bounds the page bounds ({@code null} not permitted).
*
* @return The new page.
*/
public Page createPage(Rectangle2D bounds) {
Page page = new Page(this.nextNumber++, 0, this.pages, bounds,
!this.debug);
this.pages.add(page);
return page;
}
/**
* Adds an object to the document.
*
* @param object the object ({@code null} not permitted).
*/
public void addObject(PDFObject object) {
Args.nullNotPermitted(object, "object");
this.otherObjects.add(object);
}
/**
* Returns a new PDF object number and increments the internal counter
* for the next PDF object number. This method is used to ensure that
* all objects in the document are assigned a unique number.
*
* @return A new PDF object number.
*/
public int getNextNumber() {
int result = this.nextNumber;
this.nextNumber++;
return result;
}
/**
* Returns a byte array containing the encoding of this PDF document.
*
* @return A byte array containing the encoding of this PDF document.
*/
public byte[] getPDFBytes() {
int[] xref = new int[this.nextNumber];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
bos.write(toBytes("%PDF-1.4\n"));
bos.write(new byte[] { (byte) 37, (byte) 128, (byte) 129,
(byte) 130, (byte) 131, (byte) 10});
xref[this.catalog.getNumber() - 1] = bos.size(); // offset to catalog
bos.write(this.catalog.toPDFBytes());
xref[this.outlines.getNumber() - 1] = bos.size(); // offset to outlines
bos.write(this.outlines.toPDFBytes());
xref[this.info.getNumber() - 1] = bos.size(); // offset to info
bos.write(this.info.toPDFBytes());
xref[this.pages.getNumber() - 1] = bos.size(); // offset to pages
bos.write(this.pages.toPDFBytes());
for (Page page : this.pages.getPages()) {
xref[page.getNumber() - 1] = bos.size();
bos.write(page.toPDFBytes());
PDFObject contents = page.getContents();
xref[contents.getNumber() - 1] = bos.size();
bos.write(contents.toPDFBytes());
}
for (PDFFont font: this.pages.getFonts()) {
xref[font.getNumber() - 1] = bos.size();
bos.write(font.toPDFBytes());
}
for (PDFObject object: this.otherObjects) {
xref[object.getNumber() - 1] = bos.size();
bos.write(object.toPDFBytes());
}
xref[xref.length - 1] = bos.size();
// write the xref table
bos.write(toBytes("xref\n"));
bos.write(toBytes("0 " + String.valueOf(this.nextNumber)
+ "\n"));
bos.write(toBytes("0000000000 65535 f \n"));
for (int i = 0; i < this.nextNumber - 1; i++) {
String offset = String.valueOf(xref[i]);
int len = offset.length();
String offset10 = "0000000000".substring(len) + offset;
bos.write(toBytes(offset10 + " 00000 n \n"));
}
// write the trailer
bos.write(toBytes("trailer\n"));
Dictionary trailer = new Dictionary();
trailer.put("/Size", Integer.valueOf(this.nextNumber));
trailer.put("/Root", this.catalog);
trailer.put("/Info", this.info);
bos.write(trailer.toPDFBytes());
bos.write(toBytes("startxref\n"));
bos.write(toBytes(String.valueOf(xref[this.nextNumber - 1])
+ "\n"));
bos.write(toBytes("%%EOF"));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return bos.toByteArray();
}
/**
* Writes the PDF document to a file. This is not a robust method, it
* exists mainly for the demo output.
*
* @param f the file.
*/
public void writeToFile(File f) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(f);
fos.write(getPDFBytes());
} catch (FileNotFoundException ex) {
LOGGER.log(Level.SEVERE, null, ex);
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, null, ex);
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, null, ex);
}
}
}
/**
* A utility method to convert a string to US-ASCII byte format.
*
* @param s the string.
*
* @return The corresponding byte array.
*/
private byte[] toBytes(String s) {
byte[] result = null;
try {
result = s.getBytes("US-ASCII");
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
return result;
}
}