0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017:
0018: /* $Id: PDFDocument.java 535883 2007-05-07 14:30:23Z jeremias $ */
0019:
0020: package org.apache.fop.pdf;
0021:
0022: // Java
0023: import java.io.IOException;
0024: import java.io.InputStream;
0025: import java.io.OutputStream;
0026: import java.io.UnsupportedEncodingException;
0027: import java.security.MessageDigest;
0028: import java.security.NoSuchAlgorithmException;
0029: import java.text.DateFormat;
0030: import java.text.SimpleDateFormat;
0031: import java.util.Collections;
0032: import java.util.Date;
0033: import java.util.List;
0034: import java.util.Map;
0035: import java.util.Iterator;
0036:
0037: import org.apache.commons.logging.Log;
0038: import org.apache.commons.logging.LogFactory;
0039:
0040: /* image support modified from work of BoBoGi */
0041: /* font support based on work by Takayuki Takeuchi */
0042:
0043: /**
0044: * class representing a PDF document.
0045: *
0046: * The document is built up by calling various methods and then finally
0047: * output to given filehandle using output method.
0048: *
0049: * A PDF document consists of a series of numbered objects preceded by a
0050: * header and followed by an xref table and trailer. The xref table
0051: * allows for quick access to objects by listing their character
0052: * positions within the document. For this reason the PDF document must
0053: * keep track of the character position of each object. The document
0054: * also keeps direct track of the /Root, /Info and /Resources objects.
0055: *
0056: * Modified by Mark Lillywhite, mark-fop@inomial.com. The changes
0057: * involve: ability to output pages one-at-a-time in a streaming
0058: * fashion (rather than storing them all for output at the end);
0059: * ability to write the /Pages object after writing the rest
0060: * of the document; ability to write to a stream and flush
0061: * the object list; enhanced trailer output; cleanups.
0062: *
0063: */
0064: public class PDFDocument {
0065:
0066: private static final Integer LOCATION_PLACEHOLDER = new Integer(0);
0067:
0068: /** Integer constant to represent PDF 1.3 */
0069: public static final int PDF_VERSION_1_3 = 3;
0070:
0071: /** Integer constant to represent PDF 1.4 */
0072: public static final int PDF_VERSION_1_4 = 4;
0073:
0074: /**
0075: * the encoding to use when converting strings to PDF commandos.
0076: */
0077: public static final String ENCODING = "ISO-8859-1";
0078:
0079: private Log log = LogFactory.getLog("org.apache.fop.pdf");
0080:
0081: /**
0082: * the current character position
0083: */
0084: protected int position = 0;
0085:
0086: /**
0087: * the character position of each object
0088: */
0089: protected List location = new java.util.ArrayList();
0090:
0091: /** List of objects to write in the trailer */
0092: private List trailerObjects = new java.util.ArrayList();
0093:
0094: /**
0095: * the counter for object numbering
0096: */
0097: protected int objectcount = 0;
0098:
0099: /**
0100: * the objects themselves
0101: */
0102: protected List objects = new java.util.LinkedList();
0103:
0104: /**
0105: * character position of xref table
0106: */
0107: protected int xref;
0108:
0109: /** Indicates what PDF version is active */
0110: protected int pdfVersion = PDF_VERSION_1_4;
0111:
0112: /**
0113: * Indicates which PDF profiles are active (PDF/A, PDF/X etc.)
0114: */
0115: protected PDFProfile pdfProfile = new PDFProfile(this );
0116:
0117: /**
0118: * the /Root object
0119: */
0120: protected PDFRoot root;
0121:
0122: /** The root outline object */
0123: private PDFOutline outlineRoot = null;
0124:
0125: /** The /Pages object (mark-fop@inomial.com) */
0126: private PDFPages pages;
0127:
0128: /**
0129: * the /Info object
0130: */
0131: protected PDFInfo info;
0132:
0133: /**
0134: * the /Resources object
0135: */
0136: protected PDFResources resources;
0137:
0138: /**
0139: * the documents encryption, if exists
0140: */
0141: protected PDFEncryption encryption;
0142:
0143: /**
0144: * the colorspace (0=RGB, 1=CMYK)
0145: */
0146: protected PDFDeviceColorSpace colorspace = new PDFDeviceColorSpace(
0147: PDFDeviceColorSpace.DEVICE_RGB);
0148:
0149: /**
0150: * the counter for Pattern name numbering (e.g. 'Pattern1')
0151: */
0152: protected int patternCount = 0;
0153:
0154: /**
0155: * the counter for Shading name numbering
0156: */
0157: protected int shadingCount = 0;
0158:
0159: /**
0160: * the counter for XObject numbering
0161: */
0162: protected int xObjectCount = 0;
0163:
0164: /**
0165: * the XObjects Map.
0166: * Should be modified (works only for image subtype)
0167: */
0168: protected Map xObjectsMap = new java.util.HashMap();
0169:
0170: /**
0171: * the Font Map.
0172: */
0173: protected Map fontMap = new java.util.HashMap();
0174:
0175: /**
0176: * The filter map.
0177: */
0178: protected Map filterMap = new java.util.HashMap();
0179:
0180: /**
0181: * List of PDFGState objects.
0182: */
0183: protected List gstates = new java.util.ArrayList();
0184:
0185: /**
0186: * List of functions.
0187: */
0188: protected List functions = new java.util.ArrayList();
0189:
0190: /**
0191: * List of shadings.
0192: */
0193: protected List shadings = new java.util.ArrayList();
0194:
0195: /**
0196: * List of patterns.
0197: */
0198: protected List patterns = new java.util.ArrayList();
0199:
0200: /**
0201: * List of Links.
0202: */
0203: protected List links = new java.util.ArrayList();
0204:
0205: /**
0206: * List of Destinations.
0207: */
0208: protected List destinations;
0209:
0210: /**
0211: * List of FileSpecs.
0212: */
0213: protected List filespecs = new java.util.ArrayList();
0214:
0215: /**
0216: * List of GoToRemotes.
0217: */
0218: protected List gotoremotes = new java.util.ArrayList();
0219:
0220: /**
0221: * List of GoTos.
0222: */
0223: protected List gotos = new java.util.ArrayList();
0224:
0225: /**
0226: * The PDFDests object for the name dictionary.
0227: * Note: This object is not a list.
0228: */
0229: private PDFDests dests;
0230:
0231: private PDFFactory factory;
0232:
0233: private boolean encodingOnTheFly = true;
0234:
0235: /**
0236: * Creates an empty PDF document.
0237: *
0238: * The constructor creates a /Root and /Pages object to
0239: * track the document but does not write these objects until
0240: * the trailer is written. Note that the object ID of the
0241: * pages object is determined now, and the xref table is
0242: * updated later. This allows Pages to refer to their
0243: * Parent before we write it out.
0244: *
0245: * @param prod the name of the producer of this pdf document
0246: */
0247: public PDFDocument(String prod) {
0248:
0249: this .factory = new PDFFactory(this );
0250:
0251: /* create the /Root, /Info and /Resources objects */
0252: this .pages = getFactory().makePages();
0253:
0254: // Create the Root object
0255: this .root = getFactory().makeRoot(pages);
0256:
0257: // Create the Resources object
0258: this .resources = getFactory().makeResources();
0259:
0260: // Make the /Info record
0261: this .info = getFactory().makeInfo(prod);
0262: }
0263:
0264: /**
0265: * @return the integer representing the active PDF version (one of PDFDocument.PDF_VERSION_*)
0266: */
0267: public int getPDFVersion() {
0268: return this .pdfVersion;
0269: }
0270:
0271: /** @return the String representing the active PDF version */
0272: public String getPDFVersionString() {
0273: switch (getPDFVersion()) {
0274: case PDF_VERSION_1_3:
0275: return "1.3";
0276: case PDF_VERSION_1_4:
0277: return "1.4";
0278: default:
0279: throw new IllegalStateException(
0280: "Unsupported PDF version selected");
0281: }
0282: }
0283:
0284: /** @return the PDF profile currently active. */
0285: public PDFProfile getProfile() {
0286: return this .pdfProfile;
0287: }
0288:
0289: /**
0290: * Returns the factory for PDF objects.
0291: * @return PDFFactory the factory
0292: */
0293: public PDFFactory getFactory() {
0294: return this .factory;
0295: }
0296:
0297: /**
0298: * Indicates whether stream encoding on-the-fly is enabled. If enabled
0299: * stream can be serialized without the need for a buffer to merely
0300: * calculate the stream length.
0301: * @return boolean true if on-the-fly encoding is enabled
0302: */
0303: public boolean isEncodingOnTheFly() {
0304: return this .encodingOnTheFly;
0305: }
0306:
0307: /**
0308: * Converts text to a byte array for writing to a PDF file.
0309: * @param text text to convert/encode
0310: * @return byte[] the resulting byte array
0311: */
0312: public static byte[] encode(String text) {
0313: try {
0314: return text.getBytes(ENCODING);
0315: } catch (UnsupportedEncodingException uee) {
0316: return text.getBytes();
0317: }
0318: }
0319:
0320: /**
0321: * set the producer of the document
0322: *
0323: * @param producer string indicating application producing the PDF
0324: */
0325: public void setProducer(String producer) {
0326: this .info.setProducer(producer);
0327: }
0328:
0329: /**
0330: * Set the creation date of the document.
0331: *
0332: * @param date Date to be stored as creation date in the PDF.
0333: */
0334: public void setCreationDate(Date date) {
0335: info.setCreationDate(date);
0336: }
0337:
0338: /**
0339: * Set the creator of the document.
0340: *
0341: * @param creator string indicating application creating the document
0342: */
0343: public void setCreator(String creator) {
0344: this .info.setCreator(creator);
0345: }
0346:
0347: /**
0348: * Set the filter map to use for filters in this document.
0349: *
0350: * @param map the map of filter lists for each stream type
0351: */
0352: public void setFilterMap(Map map) {
0353: this .filterMap = map;
0354: }
0355:
0356: /**
0357: * Get the filter map used for filters in this document.
0358: *
0359: * @return the map of filters being used
0360: */
0361: public Map getFilterMap() {
0362: return this .filterMap;
0363: }
0364:
0365: /**
0366: * Returns the PDFPages object associated with the root object.
0367: * @return the PDFPages object
0368: */
0369: public PDFPages getPages() {
0370: return this .pages;
0371: }
0372:
0373: /**
0374: * Get the PDF root object.
0375: *
0376: * @return the PDFRoot object
0377: */
0378: public PDFRoot getRoot() {
0379: return this .root;
0380: }
0381:
0382: /**
0383: * Get the pdf info object for this document.
0384: *
0385: * @return the PDF Info object for this document
0386: */
0387: public PDFInfo getInfo() {
0388: return info;
0389: }
0390:
0391: /**
0392: * Registers a PDFObject in this PDF document. The PDF is assigned a new
0393: * object number.
0394: * @param obj PDFObject to add
0395: * @return PDFObject the PDFObject added (its object number set)
0396: */
0397: public PDFObject registerObject(PDFObject obj) {
0398: assignObjectNumber(obj);
0399: addObject(obj);
0400: return obj;
0401: }
0402:
0403: /**
0404: * Assigns the PDFObject a object number and sets the parent of the
0405: * PDFObject to this PDFDocument.
0406: * @param obj PDFObject to assign a number to
0407: */
0408: public void assignObjectNumber(PDFObject obj) {
0409: if (obj == null) {
0410: throw new NullPointerException("obj must not be null");
0411: }
0412: if (obj.hasObjectNumber()) {
0413: throw new IllegalStateException(
0414: "Error registering a PDFObject: "
0415: + "PDFObject already has an object number");
0416: }
0417: PDFDocument currentParent = obj.getDocument();
0418: if (currentParent != null && currentParent != this ) {
0419: throw new IllegalStateException(
0420: "Error registering a PDFObject: "
0421: + "PDFObject already has a parent PDFDocument");
0422: }
0423:
0424: obj.setObjectNumber(++this .objectcount);
0425:
0426: if (currentParent == null) {
0427: obj.setDocument(this );
0428: }
0429: }
0430:
0431: /**
0432: * Adds an PDFObject to this document. The object must have a object number
0433: * assigned.
0434: * @param obj PDFObject to add
0435: */
0436: public void addObject(PDFObject obj) {
0437: if (obj == null) {
0438: throw new NullPointerException("obj must not be null");
0439: }
0440: if (!obj.hasObjectNumber()) {
0441: throw new IllegalStateException(
0442: "Error adding a PDFObject: "
0443: + "PDFObject doesn't have an object number");
0444: }
0445:
0446: //Add object to list
0447: this .objects.add(obj);
0448:
0449: //Add object to special lists where necessary
0450: if (obj instanceof PDFFunction) {
0451: this .functions.add(obj);
0452: }
0453: if (obj instanceof PDFShading) {
0454: final String shadingName = "Sh" + (++this .shadingCount);
0455: ((PDFShading) obj).setName(shadingName);
0456: this .shadings.add(obj);
0457: }
0458: if (obj instanceof PDFPattern) {
0459: final String patternName = "Pa" + (++this .patternCount);
0460: ((PDFPattern) obj).setName(patternName);
0461: this .patterns.add(obj);
0462: }
0463: if (obj instanceof PDFFont) {
0464: final PDFFont font = (PDFFont) obj;
0465: this .fontMap.put(font.getName(), font);
0466: }
0467: if (obj instanceof PDFGState) {
0468: this .gstates.add(obj);
0469: }
0470: if (obj instanceof PDFPage) {
0471: this .pages.notifyKidRegistered((PDFPage) obj);
0472: }
0473: if (obj instanceof PDFLink) {
0474: this .links.add(obj);
0475: }
0476: if (obj instanceof PDFFileSpec) {
0477: this .filespecs.add(obj);
0478: }
0479: if (obj instanceof PDFGoToRemote) {
0480: this .gotoremotes.add(obj);
0481: }
0482: }
0483:
0484: /**
0485: * Add trailer object.
0486: * Adds an object to the list of trailer objects.
0487: *
0488: * @param obj the PDF object to add
0489: */
0490: public void addTrailerObject(PDFObject obj) {
0491: this .trailerObjects.add(obj);
0492:
0493: if (obj instanceof PDFGoTo) {
0494: this .gotos.add(obj);
0495: }
0496: }
0497:
0498: /**
0499: * Apply the encryption filter to a PDFStream if encryption is enabled.
0500: * @param stream PDFStream to encrypt
0501: */
0502: public void applyEncryption(AbstractPDFStream stream) {
0503: if (isEncryptionActive()) {
0504: this .encryption.applyFilter(stream);
0505: }
0506: }
0507:
0508: /**
0509: * Enables PDF encryption.
0510: * @param params The encryption parameters for the pdf file
0511: */
0512: public void setEncryption(PDFEncryptionParams params) {
0513: getProfile().verifyEncryptionAllowed();
0514: this .encryption = PDFEncryptionManager.newInstance(
0515: ++this .objectcount, params);
0516: ((PDFObject) this .encryption).setDocument(this );
0517: if (encryption != null) {
0518: /**@todo this cast is ugly. PDFObject should be transformed to an interface. */
0519: addTrailerObject((PDFObject) this .encryption);
0520: } else {
0521: log.warn("PDF encryption is unavailable. PDF will be "
0522: + "generated without encryption.");
0523: }
0524: }
0525:
0526: /**
0527: * Indicates whether encryption is active for this PDF or not.
0528: * @return boolean True if encryption is active
0529: */
0530: public boolean isEncryptionActive() {
0531: return this .encryption != null;
0532: }
0533:
0534: /**
0535: * Returns the active Encryption object.
0536: * @return the Encryption object
0537: */
0538: public PDFEncryption getEncryption() {
0539: return encryption;
0540: }
0541:
0542: private Object findPDFObject(List list, PDFObject compare) {
0543: for (Iterator iter = list.iterator(); iter.hasNext();) {
0544: Object obj = iter.next();
0545: if (compare.equals(obj)) {
0546: return obj;
0547: }
0548: }
0549: return null;
0550: }
0551:
0552: /**
0553: * Looks through the registered functions to see if one that is equal to
0554: * a reference object exists
0555: * @param compare reference object
0556: * @return the function if it was found, null otherwise
0557: */
0558: protected PDFFunction findFunction(PDFFunction compare) {
0559: return (PDFFunction) findPDFObject(functions, compare);
0560: }
0561:
0562: /**
0563: * Looks through the registered shadings to see if one that is equal to
0564: * a reference object exists
0565: * @param compare reference object
0566: * @return the shading if it was found, null otherwise
0567: */
0568: protected PDFShading findShading(PDFShading compare) {
0569: return (PDFShading) findPDFObject(shadings, compare);
0570: }
0571:
0572: /**
0573: * Find a previous pattern.
0574: * The problem with this is for tiling patterns the pattern
0575: * data stream is stored and may use up memory, usually this
0576: * would only be a small amount of data.
0577: * @param compare reference object
0578: * @return the shading if it was found, null otherwise
0579: */
0580: protected PDFPattern findPattern(PDFPattern compare) {
0581: return (PDFPattern) findPDFObject(patterns, compare);
0582: }
0583:
0584: /**
0585: * Finds a font.
0586: * @param fontname name of the font
0587: * @return PDFFont the requested font, null if it wasn't found
0588: */
0589: protected PDFFont findFont(String fontname) {
0590: return (PDFFont) fontMap.get(fontname);
0591: }
0592:
0593: /**
0594: * Finds a named destination.
0595: * @param compare reference object to use as search template
0596: * @return the link if found, null otherwise
0597: */
0598: protected PDFDestination findDestination(PDFDestination compare) {
0599: int index = getDestinationList().indexOf(compare);
0600: if (index >= 0) {
0601: return (PDFDestination) getDestinationList().get(index);
0602: } else {
0603: return null;
0604: }
0605: }
0606:
0607: /**
0608: * Finds a link.
0609: * @param compare reference object to use as search template
0610: * @return the link if found, null otherwise
0611: */
0612: protected PDFLink findLink(PDFLink compare) {
0613: return (PDFLink) findPDFObject(links, compare);
0614: }
0615:
0616: /**
0617: * Finds a file spec.
0618: * @param compare reference object to use as search template
0619: * @return the file spec if found, null otherwise
0620: */
0621: protected PDFFileSpec findFileSpec(PDFFileSpec compare) {
0622: return (PDFFileSpec) findPDFObject(filespecs, compare);
0623: }
0624:
0625: /**
0626: * Finds a goto remote.
0627: * @param compare reference object to use as search template
0628: * @return the goto remote if found, null otherwise
0629: */
0630: protected PDFGoToRemote findGoToRemote(PDFGoToRemote compare) {
0631: return (PDFGoToRemote) findPDFObject(gotoremotes, compare);
0632: }
0633:
0634: /**
0635: * Finds a goto.
0636: * @param compare reference object to use as search template
0637: * @return the goto if found, null otherwise
0638: */
0639: protected PDFGoTo findGoTo(PDFGoTo compare) {
0640: return (PDFGoTo) findPDFObject(gotos, compare);
0641: }
0642:
0643: /**
0644: * Looks for an existing GState to use
0645: * @param wanted requested features
0646: * @param current currently active features
0647: * @return PDFGState the GState if found, null otherwise
0648: */
0649: protected PDFGState findGState(PDFGState wanted, PDFGState current) {
0650: PDFGState poss;
0651: Iterator iter = gstates.iterator();
0652: while (iter.hasNext()) {
0653: PDFGState avail = (PDFGState) iter.next();
0654: poss = new PDFGState();
0655: poss.addValues(current);
0656: poss.addValues(avail);
0657: if (poss.equals(wanted)) {
0658: return avail;
0659: }
0660: }
0661: return null;
0662: }
0663:
0664: /**
0665: * Get the PDF color space object.
0666: *
0667: * @return the color space
0668: */
0669: public PDFDeviceColorSpace getPDFColorSpace() {
0670: return this .colorspace;
0671: }
0672:
0673: /**
0674: * Get the color space.
0675: *
0676: * @return the color space
0677: */
0678: public int getColorSpace() {
0679: return getPDFColorSpace().getColorSpace();
0680: }
0681:
0682: /**
0683: * Set the color space.
0684: * This is used when creating gradients.
0685: *
0686: * @param theColorspace the new color space
0687: */
0688: public void setColorSpace(int theColorspace) {
0689: this .colorspace.setColorSpace(theColorspace);
0690: return;
0691: }
0692:
0693: /**
0694: * Get the font map for this document.
0695: *
0696: * @return the map of fonts used in this document
0697: */
0698: public Map getFontMap() {
0699: return fontMap;
0700: }
0701:
0702: /**
0703: * Resolve a URI.
0704: *
0705: * @param uri the uri to resolve
0706: * @throws java.io.FileNotFoundException if the URI could not be resolved
0707: * @return the InputStream from the URI.
0708: */
0709: protected InputStream resolveURI(String uri)
0710: throws java.io.FileNotFoundException {
0711: try {
0712: /**@todo Temporary hack to compile, improve later */
0713: return new java.net.URL(uri).openStream();
0714: } catch (Exception e) {
0715: throw new java.io.FileNotFoundException(
0716: "URI could not be resolved (" + e.getMessage()
0717: + "): " + uri);
0718: }
0719: }
0720:
0721: /**
0722: * Get an image from the image map.
0723: *
0724: * @param key the image key to look for
0725: * @return the image or PDFXObject for the key if found
0726: */
0727: public PDFXObject getImage(String key) {
0728: PDFXObject xObject = (PDFXObject) xObjectsMap.get(key);
0729: return xObject;
0730: }
0731:
0732: /**
0733: * Gets the PDFDests object (which represents the /Dests entry).
0734: *
0735: * @return the PDFDests object (which represents the /Dests entry).
0736: */
0737: public PDFDests getDests() {
0738: return dests;
0739: }
0740:
0741: /**
0742: * Adds a destination to the document.
0743: * @param destination the destination object
0744: */
0745: public void addDestination(PDFDestination destination) {
0746: if (this .destinations == null) {
0747: this .destinations = new java.util.ArrayList();
0748: }
0749: this .destinations.add(destination);
0750: }
0751:
0752: /**
0753: * Gets the list of named destinations.
0754: *
0755: * @return the list of named destinations.
0756: */
0757: public List getDestinationList() {
0758: if (hasDestinations()) {
0759: return destinations;
0760: } else {
0761: return Collections.EMPTY_LIST;
0762: }
0763: }
0764:
0765: /**
0766: * Gets whether the document has named destinations.
0767: *
0768: * @return whether the document has named destinations.
0769: */
0770: public boolean hasDestinations() {
0771: return this .destinations != null
0772: && !this .destinations.isEmpty();
0773: }
0774:
0775: /**
0776: * Add an image to the PDF document.
0777: * This adds an image to the PDF objects.
0778: * If an image with the same key already exists it will return the
0779: * old PDFXObject.
0780: *
0781: * @param res the PDF resource context to add to, may be null
0782: * @param img the PDF image to add
0783: * @return the PDF XObject that references the PDF image data
0784: */
0785: public PDFXObject addImage(PDFResourceContext res, PDFImage img) {
0786: // check if already created
0787: String key = img.getKey();
0788: PDFXObject xObject = (PDFXObject) xObjectsMap.get(key);
0789: if (xObject != null) {
0790: if (res != null) {
0791: res.getPDFResources().addXObject(xObject);
0792: }
0793: return xObject;
0794: }
0795:
0796: // setup image
0797: img.setup(this );
0798: // create a new XObject
0799: xObject = new PDFXObject(++this .xObjectCount, img);
0800: registerObject(xObject);
0801: this .resources.addXObject(xObject);
0802: if (res != null) {
0803: res.getPDFResources().addXObject(xObject);
0804: }
0805: this .xObjectsMap.put(key, xObject);
0806: return xObject;
0807: }
0808:
0809: /**
0810: * Add a form XObject to the PDF document.
0811: * This adds a Form XObject to the PDF objects.
0812: * If a Form XObject with the same key already exists it will return the
0813: * old PDFFormXObject.
0814: *
0815: * @param res the PDF resource context to add to, may be null
0816: * @param cont the PDF Stream contents of the Form XObject
0817: * @param formres the PDF Resources for the Form XObject data
0818: * @param key the key for the object
0819: * @return the PDF Form XObject that references the PDF data
0820: */
0821: public PDFFormXObject addFormXObject(PDFResourceContext res,
0822: PDFStream cont, PDFResources formres, String key) {
0823: PDFFormXObject xObject;
0824: xObject = new PDFFormXObject(++this .xObjectCount, cont, formres
0825: .referencePDF());
0826: registerObject(xObject);
0827: this .resources.addXObject(xObject);
0828: if (res != null) {
0829: res.getPDFResources().addXObject(xObject);
0830: }
0831: return xObject;
0832: }
0833:
0834: /**
0835: * Get the root Outlines object. This method does not write
0836: * the outline to the PDF document, it simply creates a
0837: * reference for later.
0838: *
0839: * @return the PDF Outline root object
0840: */
0841: public PDFOutline getOutlineRoot() {
0842: if (outlineRoot != null) {
0843: return outlineRoot;
0844: }
0845:
0846: outlineRoot = new PDFOutline(null, null, true);
0847: assignObjectNumber(outlineRoot);
0848: addTrailerObject(outlineRoot);
0849: root.setRootOutline(outlineRoot);
0850: return outlineRoot;
0851: }
0852:
0853: /**
0854: * get the /Resources object for the document
0855: *
0856: * @return the /Resources object
0857: */
0858: public PDFResources getResources() {
0859: return this .resources;
0860: }
0861:
0862: /**
0863: * Ensure there is room in the locations xref for the number of
0864: * objects that have been created.
0865: */
0866: private void setLocation(int objidx, int position) {
0867: while (location.size() <= objidx) {
0868: location.add(LOCATION_PLACEHOLDER);
0869: }
0870: location.set(objidx, new Integer(position));
0871: }
0872:
0873: /**
0874: * write the entire document out
0875: *
0876: * @param stream the OutputStream to output the document to
0877: * @throws IOException if there is an exception writing to the output stream
0878: */
0879: public void output(OutputStream stream) throws IOException {
0880: //Write out objects until the list is empty. This approach (used with a
0881: //LinkedList) allows for output() methods to create and register objects
0882: //on the fly even during serialization.
0883: while (this .objects.size() > 0) {
0884: /* Retrieve first */
0885: PDFObject object = (PDFObject) this .objects.remove(0);
0886: /*
0887: * add the position of this object to the list of object
0888: * locations
0889: */
0890: setLocation(object.getObjectNumber() - 1, this .position);
0891:
0892: /*
0893: * output the object and increment the character position
0894: * by the object's length
0895: */
0896: this .position += object.output(stream);
0897: }
0898:
0899: //Clear all objects written to the file
0900: //this.objects.clear();
0901: }
0902:
0903: /**
0904: * Write the PDF header.
0905: *
0906: * This method must be called prior to formatting
0907: * and outputting AreaTrees.
0908: *
0909: * @param stream the OutputStream to write the header to
0910: * @throws IOException if there is an exception writing to the output stream
0911: */
0912: public void outputHeader(OutputStream stream) throws IOException {
0913: this .position = 0;
0914:
0915: getProfile().verifyPDFVersion();
0916:
0917: byte[] pdf = encode("%PDF-" + getPDFVersionString() + "\n");
0918: stream.write(pdf);
0919: this .position += pdf.length;
0920:
0921: // output a binary comment as recommended by the PDF spec (3.4.1)
0922: byte[] bin = { (byte) '%', (byte) 0xAA, (byte) 0xAB,
0923: (byte) 0xAC, (byte) 0xAD, (byte) '\n' };
0924: stream.write(bin);
0925: this .position += bin.length;
0926: }
0927:
0928: /** @return the "ID" entry for the file trailer */
0929: protected String getIDEntry() {
0930: try {
0931: MessageDigest digest = MessageDigest.getInstance("MD5");
0932: DateFormat df = new SimpleDateFormat(
0933: "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS");
0934: digest.update(encode(df.format(new Date())));
0935: //Ignoring the filename here for simplicity even though it's recommended by the PDF spec
0936: digest.update(encode(String.valueOf(this .position)));
0937: digest.update(getInfo().toPDF());
0938: byte[] res = digest.digest();
0939: String s = PDFText.toHex(res);
0940: return "/ID [" + s + " " + s + "]";
0941: } catch (NoSuchAlgorithmException e) {
0942: if (getProfile().isIDEntryRequired()) {
0943: throw new UnsupportedOperationException(
0944: "MD5 not available: " + e.getMessage());
0945: } else {
0946: return ""; //Entry is optional if PDF/A or PDF/X are not active
0947: }
0948: }
0949: }
0950:
0951: /**
0952: * write the trailer
0953: *
0954: * @param stream the OutputStream to write the trailer to
0955: * @throws IOException if there is an exception writing to the output stream
0956: */
0957: public void outputTrailer(OutputStream stream) throws IOException {
0958: if (hasDestinations()) {
0959: Collections.sort(destinations, new DestinationComparator());
0960: dests = getFactory().makeDests(destinations);
0961: if (this .root.getNames() == null) {
0962: this .root.setNames(getFactory().makeNames());
0963: }
0964: this .root.getNames().setDests(dests);
0965: }
0966: output(stream);
0967: for (int count = 0; count < trailerObjects.size(); count++) {
0968: PDFObject o = (PDFObject) trailerObjects.get(count);
0969: this .location.set(o.getObjectNumber() - 1, new Integer(
0970: this .position));
0971: this .position += o.output(stream);
0972: }
0973: /* output the xref table and increment the character position
0974: by the table's length */
0975: this .position += outputXref(stream);
0976:
0977: // Determine existance of encryption dictionary
0978: String encryptEntry = "";
0979: if (this .encryption != null) {
0980: encryptEntry = this .encryption.getTrailerEntry();
0981: }
0982:
0983: /* construct the trailer */
0984: String pdf = "trailer\n" + "<<\n" + "/Size "
0985: + (this .objectcount + 1) + "\n" + "/Root "
0986: + this .root.referencePDF() + "\n" + "/Info "
0987: + this .info.referencePDF() + "\n" + getIDEntry() + "\n"
0988: + encryptEntry + ">>\n" + "startxref\n" + this .xref
0989: + "\n" + "%%EOF\n";
0990:
0991: /* write the trailer */
0992: stream.write(encode(pdf));
0993: }
0994:
0995: /**
0996: * write the xref table
0997: *
0998: * @param stream the OutputStream to write the xref table to
0999: * @return the number of characters written
1000: */
1001: private int outputXref(OutputStream stream) throws IOException {
1002:
1003: /* remember position of xref table */
1004: this .xref = this .position;
1005:
1006: /* construct initial part of xref */
1007: StringBuffer pdf = new StringBuffer(128);
1008: pdf.append("xref\n0 " + (this .objectcount + 1)
1009: + "\n0000000000 65535 f \n");
1010:
1011: for (int count = 0; count < this .location.size(); count++) {
1012: String x = this .location.get(count).toString();
1013:
1014: /* contruct xref entry for object */
1015: String padding = "0000000000";
1016: String loc = padding.substring(x.length()) + x;
1017:
1018: /* append to xref table */
1019: pdf = pdf.append(loc + " 00000 n \n");
1020: }
1021:
1022: /* write the xref table and return the character length */
1023: byte[] pdfBytes = encode(pdf.toString());
1024: stream.write(pdfBytes);
1025: return pdfBytes.length;
1026: }
1027:
1028: }
|