/* * $Id: PDFDocument.java,v 1.63 2003/03/07 08:25:47 jeremias Exp $ * ============================================================================ * The Apache Software License, Version 1.1 * ============================================================================ * * Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without modifica- * tion, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by the Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "FOP" and "Apache Software Foundation" must not be used to * endorse or promote products derived from this software without prior * written permission. For written permission, please contact * apache@apache.org. * * 5. Products derived from this software may not be called "Apache", nor may * "Apache" appear in their name, without prior written permission of the * Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- * DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * ============================================================================ * * This software consists of voluntary contributions made by many individuals * on behalf of the Apache Software Foundation and was originally created by * James Tauber . For more information on the Apache * Software Foundation, please see . */ package org.apache.fop.pdf; // Java import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import java.util.Iterator; // Avalon import org.apache.avalon.framework.logger.LogEnabled; import org.apache.avalon.framework.logger.Logger; /* image support modified from work of BoBoGi */ /* font support based on work by Takayuki Takeuchi */ /** * class representing a PDF document. * * The document is built up by calling various methods and then finally * output to given filehandle using output method. * * A PDF document consists of a series of numbered objects preceded by a * header and followed by an xref table and trailer. The xref table * allows for quick access to objects by listing their character * positions within the document. For this reason the PDF document must * keep track of the character position of each object. The document * also keeps direct track of the /Root, /Info and /Resources objects. * * Modified by Mark Lillywhite, mark-fop@inomial.com. The changes * involve: ability to output pages one-at-a-time in a streaming * fashion (rather than storing them all for output at the end); * ability to write the /Pages object after writing the rest * of the document; ability to write to a stream and flush * the object list; enhanced trailer output; cleanups. * */ public class PDFDocument implements LogEnabled { private static final Integer LOCATION_PLACEHOLDER = new Integer(0); /** * the version of PDF supported which is 1.4 */ protected static final String PDF_VERSION = "1.4"; /** * the encoding to use when converting strings to PDF commandos. */ public static final String ENCODING = "ISO-8859-1"; private Logger logger; /** * the current character position */ protected int position = 0; /** * the character position of each object */ protected List location = new java.util.ArrayList(); /** List of objects to write in the trailer */ private List trailerObjects = new java.util.ArrayList(); /** * the counter for object numbering */ protected int objectcount = 0; /** * the objects themselves */ protected List objects = new java.util.LinkedList(); /** * character position of xref table */ protected int xref; /** * the /Root object */ protected PDFRoot root; /** The root outline object */ private PDFOutline outlineRoot = null; /** The /Pages object (mark-fop@inomial.com) */ private PDFPages pages; /** * the /Info object */ protected PDFInfo info; /** * the /Resources object */ protected PDFResources resources; /** * the documents encryption, if exists */ protected PDFEncryption encryption; /** * the colorspace (0=RGB, 1=CMYK) */ protected PDFColorSpace colorspace = new PDFColorSpace(PDFColorSpace.DEVICE_RGB); /** * the counter for Pattern name numbering (e.g. 'Pattern1') */ protected int patternCount = 0; /** * the counter for Shading name numbering */ protected int shadingCount = 0; /** * the counter for XObject numbering */ protected int xObjectCount = 0; /** * the XObjects Map. * Should be modified (works only for image subtype) */ protected Map xObjectsMap = new java.util.HashMap(); /** * the Font Map. */ protected Map fontMap = new java.util.HashMap(); /** * The filter map. */ protected Map filterMap = new java.util.HashMap(); /** * List of PDFGState objects. */ protected List gstates = new java.util.ArrayList(); /** * List of functions. */ protected List functions = new java.util.ArrayList(); /** * List of shadings. */ protected List shadings = new java.util.ArrayList(); /** * List of patterns. */ protected List patterns = new java.util.ArrayList(); /** * List of Links. */ protected List links = new java.util.ArrayList(); /** * List of FileSpecs. */ protected List filespecs = new java.util.ArrayList(); /** * List of GoToRemotes. */ protected List gotoremotes = new java.util.ArrayList(); /** * List of GoTos. */ protected List gotos = new java.util.ArrayList(); private PDFFactory factory; private boolean encodingOnTheFly = true; /** * Creates an empty PDF document. * * The constructor creates a /Root and /Pages object to * track the document but does not write these objects until * the trailer is written. Note that the object ID of the * pages object is determined now, and the xref table is * updated later. This allows Pages to refer to their * Parent before we write it out. * * @param prod the name of the producer of this pdf document */ public PDFDocument(String prod) { this.factory = new PDFFactory(this); /* create the /Root, /Info and /Resources objects */ this.pages = getFactory().makePages(); // Create the Root object this.root = getFactory().makeRoot(pages); // Create the Resources object this.resources = getFactory().makeResources(); // Make the /Info record this.info = getFactory().makeInfo(prod); } /** * Returns the factory for PDF objects. * @return PDFFactory the factory */ public PDFFactory getFactory() { return this.factory; } /** * Indicates whether stream encoding on-the-fly is enabled. If enabled * stream can be serialized without the need for a buffer to merely * calculate the stream length. * @return boolean true if on-the-fly encoding is enabled */ public boolean isEncodingOnTheFly() { return this.encodingOnTheFly; } /** * @see org.apache.avalon.framework.logger.LogEnabled#enableLogging(Logger) */ public void enableLogging(Logger logger) { this.logger = logger; } /** * Helper method to allow sub-classes to aquire logger. * *

There is no performance penalty as this is a final method * and will be inlined by the JVM.

* @return the Logger */ protected final Logger getLogger() { return this.logger; } /** * Converts text to a byte array for writing to a PDF file. * @param text text to convert/encode * @return byte[] the resulting byte array */ public static byte[] encode(String text) { try { return text.getBytes(ENCODING); } catch (UnsupportedEncodingException uee) { return text.getBytes(); } } /** * set the producer of the document * * @param producer string indicating application producing the PDF */ public void setProducer(String producer) { this.info.setProducer(producer); } /** * set the creator of the document * * @param creator string indicating application creating the document */ public void setCreator(String creator) { this.info.setCreator(creator); } /** * Set the filter map to use for filters in this document. * * @param map the map of filter lists for each stream type */ public void setFilterMap(Map map) { this.filterMap = map; } /** * Get the filter map used for filters in this document. * * @return the map of filters being used */ public Map getFilterMap() { return this.filterMap; } /** * Returns the PDFPages object associated with the root object. * @return the PDFPages object */ public PDFPages getPages() { return this.pages; } /** * Get the PDF root object. * * @return the PDFRoot object */ public PDFRoot getRoot() { return this.root; } /** * Get the pdf info object for this document. * * @return the PDF Info object for this document */ public PDFInfo getInfo() { return info; } /** * Registers a PDFObject in this PDF document. The PDF is assigned a new * object number. * @param obj PDFObject to add * @return PDFObject the PDFObject added (its object number set) */ public PDFObject registerObject(PDFObject obj) { assignObjectNumber(obj); addObject(obj); return obj; } /** * Assigns the PDFObject a object number and sets the parent of the * PDFObject to this PDFDocument. * @param obj PDFObject to assign a number to */ public void assignObjectNumber(PDFObject obj) { if (obj == null) { throw new NullPointerException("obj must not be null"); } if (obj.hasObjectNumber()) { throw new IllegalStateException("Error registering a PDFObject: " + "PDFObject already has an object number"); } PDFDocument currentParent = obj.getDocument(); if (currentParent != null && currentParent != this) { throw new IllegalStateException("Error registering a PDFObject: " + "PDFObject already has a parent PDFDocument"); } obj.setObjectNumber(++this.objectcount); if (currentParent == null) { obj.setDocument(this); } } /** * Adds an PDFObject to this document. The object must have a object number * assigned. * @param obj PDFObject to add */ public void addObject(PDFObject obj) { if (obj == null) { throw new NullPointerException("obj must not be null"); } if (!obj.hasObjectNumber()) { throw new IllegalStateException("Error adding a PDFObject: " + "PDFObject doesn't have an object number"); } //Add object to list this.objects.add(obj); //System.out.println("Registering: "+obj); //Add object to special lists where necessary if (obj instanceof PDFFunction) { this.functions.add(obj); } if (obj instanceof PDFShading) { final String shadingName = "Sh" + (++this.shadingCount); ((PDFShading)obj).setName(shadingName); this.shadings.add(obj); } if (obj instanceof PDFPattern) { final String patternName = "Pa" + (++this.patternCount); ((PDFPattern)obj).setName(patternName); this.patterns.add(obj); } if (obj instanceof PDFFont) { final PDFFont font = (PDFFont)obj; this.fontMap.put(font.getName(), font); } if (obj instanceof PDFGState) { this.gstates.add(obj); } if (obj instanceof PDFPage) { this.pages.notifyKidRegistered((PDFPage)obj); } if (obj instanceof PDFLink) { this.links.add(obj); } if (obj instanceof PDFFileSpec) { this.filespecs.add(obj); } if (obj instanceof PDFGoToRemote) { this.gotoremotes.add(obj); } } /** * Add trailer object. * Adds an object to the list of trailer objects. * * @param obj the PDF object to add */ public void addTrailerObject(PDFObject obj) { this.trailerObjects.add(obj); if (obj instanceof PDFGoTo) { this.gotos.add(obj); } } /** * Apply the encryption filter to a PDFStream if encryption is enabled. * @param stream PDFStream to encrypt */ public void applyEncryption(AbstractPDFStream stream) { if (isEncryptionActive()) { this.encryption.applyFilter(stream); } } /** * Enables PDF encryption. * @param params The encryption parameters for the pdf file */ public void setEncryption(PDFEncryptionParams params) { this.encryption = PDFEncryptionManager.newInstance(++this.objectcount, params); ((PDFObject)this.encryption).setDocument(this); if (encryption != null) { /**@todo this cast is ugly. PDFObject should be transformed to an interface. */ addTrailerObject((PDFObject)this.encryption); } else { getLogger().warn("PDF encryption is unavailable. PDF will be " + "generated without encryption."); } } /** * Indicates whether encryption is active for this PDF or not. * @return boolean True if encryption is active */ public boolean isEncryptionActive() { return this.encryption != null; } /** * Returns the active Encryption object. * @return the Encryption object */ public PDFEncryption getEncryption() { return encryption; } private Object findPDFObject(List list, PDFObject compare) { for (Iterator iter = list.iterator(); iter.hasNext();) { Object obj = iter.next(); if (compare.equals(obj)) { return obj; } } return null; } /** * Looks through the registered functions to see if one that is equal to * a reference object exists * @param compare reference object * @return the function if it was found, null otherwise */ protected PDFFunction findFunction(PDFFunction compare) { return (PDFFunction)findPDFObject(functions, compare); } /** * Looks through the registered shadings to see if one that is equal to * a reference object exists * @param compare reference object * @return the shading if it was found, null otherwise */ protected PDFShading findShading(PDFShading compare) { return (PDFShading)findPDFObject(shadings, compare); } /** * Find a previous pattern. * The problem with this is for tiling patterns the pattern * data stream is stored and may use up memory, usually this * would only be a small amount of data. * @param compare reference object * @return the shading if it was found, null otherwise */ protected PDFPattern findPattern(PDFPattern compare) { return (PDFPattern)findPDFObject(patterns, compare); } /** * Finds a font. * @param fontname name of the font * @return PDFFont the requested font, null if it wasn't found */ protected PDFFont findFont(String fontname) { return (PDFFont)fontMap.get(fontname); } /** * Finds a link. * @param compare reference object to use as search template * @return the link if found, null otherwise */ protected PDFLink findLink(PDFLink compare) { return (PDFLink)findPDFObject(links, compare); } /** * Finds a file spec. * @param compare reference object to use as search template * @return the file spec if found, null otherwise */ protected PDFFileSpec findFileSpec(PDFFileSpec compare) { return (PDFFileSpec)findPDFObject(filespecs, compare); } /** * Finds a goto remote. * @param compare reference object to use as search template * @return the goto remote if found, null otherwise */ protected PDFGoToRemote findGoToRemote(PDFGoToRemote compare) { return (PDFGoToRemote)findPDFObject(gotoremotes, compare); } /** * Finds a goto. * @param compare reference object to use as search template * @return the goto if found, null otherwise */ protected PDFGoTo findGoTo(PDFGoTo compare) { return (PDFGoTo)findPDFObject(gotos, compare); } /** * Looks for an existing GState to use * @param wanted requested features * @param current currently active features * @return PDFGState the GState if found, null otherwise */ protected PDFGState findGState(PDFGState wanted, PDFGState current) { PDFGState poss; Iterator iter = gstates.iterator(); while (iter.hasNext()) { PDFGState avail = (PDFGState)iter.next(); poss = new PDFGState(); poss.addValues(current); poss.addValues(avail); if (poss.equals(wanted)) { return avail; } } return null; } /** * Get the PDF color space object. * * @return the color space */ public PDFColorSpace getPDFColorSpace() { return this.colorspace; } /** * Get the color space. * * @return the color space */ public int getColorSpace() { return getPDFColorSpace().getColorSpace(); } /** * Set the color space. * This is used when creating gradients. * * @param theColorspace the new color space */ public void setColorSpace(int theColorspace) { this.colorspace.setColorSpace(theColorspace); return; } /** * Get the font map for this document. * * @return the map of fonts used in this document */ public Map getFontMap() { return fontMap; } /** * Resolve a URI. * * @param uri the uri to resolve * @throws java.io.FileNotFoundException if the URI could not be resolved * @return the InputStream from the URI. */ protected InputStream resolveURI(String uri) throws java.io.FileNotFoundException { try { /**@todo Temporary hack to compile, improve later */ return new java.net.URL(uri).openStream(); } catch (Exception e) { throw new java.io.FileNotFoundException( "URI could not be resolved (" + e.getMessage() + "): " + uri); } } /** * Get an image from the image map. * * @param key the image key to look for * @return the image or PDFXObject for the key if found */ public PDFXObject getImage(String key) { PDFXObject xObject = (PDFXObject)xObjectsMap.get(key); return xObject; } /** * Add an image to the PDF document. * This adds an image to the PDF objects. * If an image with the same key already exists it will return the * old PDFXObject. * * @param res the PDF resource context to add to, may be null * @param img the PDF image to add * @return the PDF XObject that references the PDF image data */ public PDFXObject addImage(PDFResourceContext res, PDFImage img) { // check if already created String key = img.getKey(); PDFXObject xObject = (PDFXObject)xObjectsMap.get(key); if (xObject != null) { if (res != null) { res.getPDFResources().addXObject(xObject); } return xObject; } // setup image img.setup(this); // create a new XObject xObject = new PDFXObject(++this.xObjectCount, img); registerObject(xObject); this.resources.addXObject(xObject); if (res != null) { res.getPDFResources().addXObject(xObject); } this.xObjectsMap.put(key, xObject); return xObject; } /** * Add a form XObject to the PDF document. * This adds a Form XObject to the PDF objects. * If a Form XObject with the same key already exists it will return the * old PDFFormXObject. * * @param res the PDF resource context to add to, may be null * @param cont the PDF Stream contents of the Form XObject * @param formres the PDF Resources for the Form XObject data * @param key the key for the object * @return the PDF Form XObject that references the PDF data */ public PDFFormXObject addFormXObject(PDFResourceContext res, PDFStream cont, PDFResources formres, String key) { PDFFormXObject xObject; xObject = new PDFFormXObject(++this.xObjectCount, cont, formres.referencePDF()); registerObject(xObject); this.resources.addXObject(xObject); if (res != null) { res.getPDFResources().addXObject(xObject); } return xObject; } /** * Get the root Outlines object. This method does not write * the outline to the PDF document, it simply creates a * reference for later. * * @return the PDF Outline root object */ public PDFOutline getOutlineRoot() { if (outlineRoot != null) { return outlineRoot; } outlineRoot = new PDFOutline(null, null); assignObjectNumber(outlineRoot); addTrailerObject(outlineRoot); root.setRootOutline(outlineRoot); return outlineRoot; } /** * get the /Resources object for the document * * @return the /Resources object */ public PDFResources getResources() { return this.resources; } /** * Ensure there is room in the locations xref for the number of * objects that have been created. */ private void setLocation(int objidx, int position) { while (location.size() <= objidx) { location.add(LOCATION_PLACEHOLDER); } location.set(objidx, new Integer(position)); } /** * write the entire document out * * @param stream the OutputStream to output the document to * @throws IOException if there is an exception writing to the output stream */ public void output(OutputStream stream) throws IOException { //Write out objects until the list is empty. This approach (used with a //LinkedList) allows for output() methods to create and register objects //on the fly even during serialization. while (this.objects.size() > 0) { /* Retrieve first */ PDFObject object = (PDFObject)this.objects.remove(0); /* * add the position of this object to the list of object * locations */ setLocation(object.getObjectNumber() - 1, this.position); /* * output the object and increment the character position * by the object's length */ this.position += object.output(stream); } //Clear all objects written to the file //this.objects.clear(); } /** * Write the PDF header. * * This method must be called prior to formatting * and outputting AreaTrees. * * @param stream the OutputStream to write the header to * @throws IOException if there is an exception writing to the output stream */ public void outputHeader(OutputStream stream) throws IOException { this.position = 0; byte[] pdf = ("%PDF-" + PDF_VERSION + "\n").getBytes(); stream.write(pdf); this.position += pdf.length; // output a binary comment as recommended by the PDF spec (3.4.1) byte[] bin = { (byte)'%', (byte)0xAA, (byte)0xAB, (byte)0xAC, (byte)0xAD, (byte)'\n' }; stream.write(bin); this.position += bin.length; } /** * write the trailer * * @param stream the OutputStream to write the trailer to * @throws IOException if there is an exception writing to the output stream */ public void outputTrailer(OutputStream stream) throws IOException { output(stream); for (int count = 0; count < trailerObjects.size(); count++) { PDFObject o = (PDFObject) trailerObjects.get(count); this.location.set(o.getObjectNumber() - 1, new Integer(this.position)); this.position += o.output(stream); } /* output the xref table and increment the character position by the table's length */ this.position += outputXref(stream); // Determine existance of encryption dictionary String encryptEntry = ""; if (this.encryption != null) { encryptEntry = this.encryption.getTrailerEntry(); } /* construct the trailer */ String pdf = "trailer\n" + "<<\n" + "/Size " + (this.objectcount + 1) + "\n" + "/Root " + this.root.referencePDF() + "\n" + "/Info " + this.info.referencePDF() + "\n" + encryptEntry + ">>\n" + "startxref\n" + this.xref + "\n" + "%%EOF\n"; /* write the trailer */ stream.write(encode(pdf)); } /** * write the xref table * * @param stream the OutputStream to write the xref table to * @return the number of characters written */ private int outputXref(OutputStream stream) throws IOException { /* remember position of xref table */ this.xref = this.position; /* construct initial part of xref */ StringBuffer pdf = new StringBuffer(128); pdf.append("xref\n0 " + (this.objectcount + 1) + "\n0000000000 65535 f \n"); for (int count = 0; count < this.location.size(); count++) { String x = this.location.get(count).toString(); /* contruct xref entry for object */ String padding = "0000000000"; String loc = padding.substring(x.length()) + x; /* append to xref table */ pdf = pdf.append(loc + " 00000 n \n"); } /* write the xref table and return the character length */ byte[] pdfBytes = encode(pdf.toString()); stream.write(pdfBytes); return pdfBytes.length; } }