| /* |
| * Copyright 1999-2004 The Apache Software Foundation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| 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.Date; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Iterator; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| /* 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 { |
| |
| 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 Log log = LogFactory.getLog("org.apache.fop.pdf"); |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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 creation date of the document. |
| * |
| * @param date Date to be stored as creation date in the PDF. |
| */ |
| public void setCreationDate(Date date) { |
| info.setCreationDate(date); |
| } |
| |
| /** |
| * 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 { |
| log.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, true); |
| 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; |
| } |
| |
| } |