blob: 2e13e70152fbd09e027a9fcad1fc7b71951ba106 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.util.SoftMapCache;
import org.apache.fop.pdf.StandardStructureAttributes.Table.Scope;
import org.apache.fop.pdf.xref.CrossReferenceStream;
import org.apache.fop.pdf.xref.CrossReferenceTable;
import org.apache.fop.pdf.xref.TrailerDictionary;
/* 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 {
/** the encoding to use when converting strings to PDF commands */
public static final String ENCODING = "ISO-8859-1";
/** the counter for object numbering */
protected int objectcount;
/** the logger instance */
private Log log = LogFactory.getLog("org.apache.fop.pdf");
/** the current character position */
protected long position;
/** the character position of each object */
protected List<Long> indirectObjectOffsets = new ArrayList<Long>();
protected List<PDFStructElem> structureTreeElements;
/** List of objects to write in the trailer */
protected List<PDFObject> trailerObjects = new ArrayList<PDFObject>();
/** the objects themselves */
protected List<PDFObject> objects = new LinkedList<PDFObject>();
/** Controls the PDF version of this document */
private VersionController versionController;
/** Indicates which PDF profiles are active (PDF/A, PDF/X etc.) */
private PDFProfile pdfProfile = new PDFProfile(this);
/** the /Root object */
private PDFRoot root;
/** The root outline object */
private PDFOutline outlineRoot;
/** The /Pages object (mark-fop@inomial.com) */
private PDFPages pages;
/** the /Info object */
private PDFInfo info;
/** the /Resources object */
private PDFResources resources;
/** the document's encryption, if it exists */
private PDFEncryption encryption;
/** the colorspace (0=RGB, 1=CMYK) */
private PDFDeviceColorSpace colorspace
= new PDFDeviceColorSpace(PDFDeviceColorSpace.DEVICE_RGB);
/** the counter for Pattern name numbering (e.g. 'Pattern1') */
private int patternCount;
/** the counter for Shading name numbering */
private int shadingCount;
/** the counter for XObject numbering */
private int xObjectCount;
protected int gStateObjectCount;
/* TODO: Should be modified (works only for image subtype) */
private Map<String, PDFXObject> xObjectsMap = new HashMap<String, PDFXObject>();
private SoftMapCache xObjectsMapFast = new SoftMapCache(false);
private Map<String, PDFFont> fontMap = new HashMap<String, PDFFont>();
private Map<String, List<String>> filterMap = new HashMap<String, List<String>>();
private List<PDFGState> gstates = new ArrayList<PDFGState>();
private List<PDFFunction> functions = new ArrayList<PDFFunction>();
private List<PDFShading> shadings = new ArrayList<PDFShading>();
private List<PDFPattern> patterns = new ArrayList<PDFPattern>();
private List<PDFLink> links = new ArrayList<PDFLink>();
private List<PDFDestination> destinations;
private List<PDFFileSpec> filespecs = new ArrayList<PDFFileSpec>();
private List<PDFGoToRemote> gotoremotes = new ArrayList<PDFGoToRemote>();
private List<PDFGoTo> gotos = new ArrayList<PDFGoTo>();
private List<PDFLaunch> launches = new ArrayList<PDFLaunch>();
protected List<PDFPage> pageObjs = new ArrayList<PDFPage>();
private List<PDFLayer> layers;
private List<PDFNavigator> navigators;
private List<PDFNavigatorAction> navigatorActions;
private PDFFactory factory;
private FileIDGenerator fileIDGenerator;
private boolean accessibilityEnabled;
private boolean mergeFontsEnabled;
private boolean linearizationEnabled;
private boolean formXObjectEnabled;
protected boolean outputStarted;
/**
* 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(prod, null);
versionController = VersionController.getDynamicVersionController(Version.V1_4, this);
}
/**
* 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
* @param versionController the version controller of this PDF document
*/
public PDFDocument(String prod, VersionController versionController) {
this.factory = new PDFFactory(this);
/* create the /Root, /Info and /Resources objects */
this.pages = getFactory().makePages();
// Create the Root object
this.root = getFactory().makeRoot(this.pages);
// Create the Resources object
this.resources = getFactory().makeResources();
// Make the /Info record
this.info = getFactory().makeInfo(prod);
this.versionController = versionController;
}
/**
* Returns the current PDF version.
*
* @return returns the PDF version
*/
public Version getPDFVersion() {
return versionController.getPDFVersion();
}
/**
* Sets the PDF version of this document.
*
* @param version the PDF version
* @throws IllegalStateException if the version of this PDF is not allowed to change.
*/
public void setPDFVersion(Version version) {
versionController.setPDFVersion(version);
}
/** @return the String representing the current PDF version */
public String getPDFVersionString() {
return versionController.getPDFVersion().toString();
}
/** @return the PDF profile currently active. */
public PDFProfile getProfile() {
return this.pdfProfile;
}
/**
* Returns the factory for PDF objects.
*
* @return the {@link PDFFactory} object
*/
public PDFFactory getFactory() {
return this.factory;
}
/**
* Converts text to a byte array for writing to a PDF file.
*
* @param text text to convert/encode
* @return the resulting <code>byte</code> array
*/
public static byte[] encode(String text) {
try {
return text.getBytes(ENCODING);
} catch (UnsupportedEncodingException uee) {
return text.getBytes();
}
}
/**
* Flushes the given text buffer to an output stream with the right encoding and resets
* the text buffer. This is used to efficiently switch between outputting text and binary
* content.
* @param textBuffer the text buffer
* @param out the output stream to flush the text content to
* @throws IOException if an I/O error occurs while writing to the output stream
*/
public static void flushTextBuffer(StringBuilder textBuffer, OutputStream out)
throws IOException {
out.write(encode(textBuffer.toString()));
textBuffer.setLength(0);
}
/**
* Sets the producer of the document.
*
* @param producer string indicating application producing the PDF
*/
public void setProducer(String producer) {
this.info.setProducer(producer);
}
/**
* Sets the creation date of the document.
*
* @param date Date to be stored as creation date in the PDF.
*/
public void setCreationDate(Date date) {
this.info.setCreationDate(date);
}
/**
* Sets the creator of the document.
*
* @param creator string indicating application creating the document
*/
public void setCreator(String creator) {
this.info.setCreator(creator);
}
/**
* Sets 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<String, List<String>> map) {
this.filterMap = map;
}
/**
* Returns the {@link PDFFilter}s map used for filters in this document.
*
* @return the map of filters being used
*/
public Map<String, List<String>> getFilterMap() {
return this.filterMap;
}
/**
* Returns the {@link PDFPages} object associated with the root object.
*
* @return the {@link PDFPages} object
*/
public PDFPages getPages() {
return this.pages;
}
/**
* Get the {@link PDFRoot} object for this document.
*
* @return the {@link PDFRoot} object
*/
public PDFRoot getRoot() {
return this.root;
}
/**
* Get the Structural Tree Collection for this document
* @return
*/
public List<PDFStructElem> getStructureTreeElements() {
return structureTreeElements;
}
/**
* Creates and returns a StructTreeRoot object.
*
* @param parentTree the value of the ParenTree entry
* @return the structure tree root
*/
public PDFStructTreeRoot makeStructTreeRoot(PDFParentTree parentTree) {
PDFStructTreeRoot structTreeRoot = new PDFStructTreeRoot(parentTree);
assignObjectNumber(structTreeRoot);
addTrailerObject(structTreeRoot);
root.setStructTreeRoot(structTreeRoot);
structureTreeElements = new ArrayList<PDFStructElem>();
return structTreeRoot;
}
/**
* Adds the given element to the structure tree.
*/
public void registerStructureElement(PDFStructElem structElem) {
assignObjectNumber(structElem);
structureTreeElements.add(structElem);
}
/**
* Assigns the given scope to the given element and adds it to the structure tree. The
* scope may not be added if it's not compatible with this document's PDF version.
*/
public void registerStructureElement(PDFStructElem structElem, Scope scope) {
registerStructureElement(structElem);
versionController.addTableHeaderScopeAttribute(structElem, scope);
}
/**
* Get the {@link PDFInfo} object for this document.
*
* @return the {@link PDFInfo} object
*/
public PDFInfo getInfo() {
return this.info;
}
/**
* Registers a {@link PDFObject} in this PDF document.
* The object is assigned a new object number.
*
* @param obj {@link PDFObject} to add
* @return the added {@link PDFObject} added (with its object number set)
*/
public PDFObject registerObject(PDFObject obj) {
assignObjectNumber(obj);
addObject(obj);
if (obj instanceof AbstractPDFStream) {
((AbstractPDFStream) obj).registerChildren();
}
return obj;
}
/**
* Registers a {@link PDFObject} in this PDF document at end.
* The object is assigned a new object number.
*
* @param obj {@link PDFObject} to add
* @return the added {@link PDFObject} added (with its object number set)
*/
<T extends PDFObject> T registerTrailerObject(T obj) {
assignObjectNumber(obj);
addTrailerObject(obj);
return obj;
}
/**
* Assigns the {@link PDFObject} an object number,
* and sets the parent of the {@link PDFObject} to this document.
*
* @param obj {@link PDFObject} to assign a number to
*/
public void assignObjectNumber(PDFObject obj) {
if (outputStarted && isLinearizationEnabled()) {
throw new IllegalStateException("Can't assign number after start of output");
}
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);
if (currentParent == null) {
obj.setDocument(this);
}
}
/**
* Adds a {@link PDFObject} to this document.
* The object <em>MUST</em> have an object number assigned.
*
* @param obj {@link 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);
//Add object to special lists where necessary
if (obj instanceof PDFFunction) {
this.functions.add((PDFFunction) obj);
}
if (obj instanceof PDFShading) {
final String shadingName = "Sh" + (++this.shadingCount);
((PDFShading)obj).setName(shadingName);
this.shadings.add((PDFShading) obj);
}
if (obj instanceof PDFPattern) {
final String patternName = "Pa" + (++this.patternCount);
((PDFPattern)obj).setName(patternName);
this.patterns.add((PDFPattern) obj);
}
if (obj instanceof PDFFont) {
final PDFFont font = (PDFFont)obj;
this.fontMap.put(font.getName(), font);
}
if (obj instanceof PDFGState) {
this.gstates.add((PDFGState) obj);
}
if (obj instanceof PDFPage) {
this.pages.notifyKidRegistered((PDFPage)obj);
pageObjs.add((PDFPage) obj);
}
if (obj instanceof PDFLaunch) {
this.launches.add((PDFLaunch) obj);
}
if (obj instanceof PDFLink) {
this.links.add((PDFLink) obj);
}
if (obj instanceof PDFFileSpec) {
this.filespecs.add((PDFFileSpec) obj);
}
if (obj instanceof PDFGoToRemote) {
this.gotoremotes.add((PDFGoToRemote) obj);
}
if (obj instanceof PDFLayer) {
if (this.layers == null) {
this.layers = new ArrayList<PDFLayer>();
}
this.layers.add((PDFLayer) obj);
}
if (obj instanceof PDFNavigator) {
if (this.navigators == null) {
this.navigators = new ArrayList<PDFNavigator>();
}
this.navigators.add((PDFNavigator) obj);
}
if (obj instanceof PDFNavigatorAction) {
if (this.navigatorActions == null) {
this.navigatorActions = new ArrayList<PDFNavigatorAction>();
}
this.navigatorActions.add((PDFNavigatorAction) 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((PDFGoTo) 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) {
getProfile().verifyEncryptionAllowed();
fileIDGenerator = FileIDGenerator.getRandomFileIDGenerator();
this.encryption = PDFEncryptionManager.newInstance(params, this);
if (this.encryption != null) {
PDFObject pdfObject = (PDFObject)this.encryption;
addTrailerObject(pdfObject);
try {
if (encryption.getPDFVersion().compareTo(versionController.getPDFVersion()) > 0) {
versionController.setPDFVersion(encryption.getPDFVersion());
}
} catch (IllegalStateException ise) {
log.warn("Configured encryption requires PDF version " + encryption.getPDFVersion()
+ " but version has been set to " + versionController.getPDFVersion() + ".");
throw ise;
}
} else {
log.warn("PDF encryption is unavailable. PDF will be generated without encryption.");
if (params.getEncryptionLengthInBits() == 256) {
log.warn("Make sure the JCE Unlimited Strength Jurisdiction Policy files are available."
+ "AES 256 encryption cannot be performed without them.");
}
}
}
/**
* 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 this.encryption;
}
private Object findPDFObject(List<? extends PDFObject> list, PDFObject compare) {
for (PDFObject obj : list) {
if (compare.contentEquals(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(this.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(this.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(this.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 this.fontMap.get(fontname);
}
/**
* Finds a named destination.
*
* @param compare reference object to use as search template
* @return the link if found, null otherwise
*/
protected PDFDestination findDestination(PDFDestination compare) {
int index = getDestinationList().indexOf(compare);
if (index >= 0) {
return getDestinationList().get(index);
} else {
return null;
}
}
/**
* 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(this.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(this.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(this.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(this.gotos, compare);
}
/**
* Finds a launch.
*
* @param compare reference object to use as search template
* @return the launch if found, null otherwise
*/
protected PDFLaunch findLaunch(PDFLaunch compare) {
return (PDFLaunch) findPDFObject(this.launches, compare);
}
/**
* Looks for an existing GState to use
*
* @param wanted requested features
* @param current currently active features
* @return the GState if found, null otherwise
*/
protected PDFGState findGState(PDFGState wanted, PDFGState current) {
PDFGState poss;
for (PDFGState avail : this.gstates) {
poss = new PDFGState();
poss.addValues(current);
poss.addValues(avail);
if (poss.contentEquals(wanted)) {
return avail;
}
}
return null;
}
/**
* Returns the PDF color space object.
*
* @return the color space
*/
public PDFDeviceColorSpace getPDFColorSpace() {
return this.colorspace;
}
/**
* Returns 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);
}
/**
* Returns the font map for this document.
*
* @return the map of fonts used in this document
*/
public Map<String, PDFFont> getFontMap() {
return this.fontMap;
}
/**
* 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
* @deprecated Use getXObject instead (so forms are treated in the same way)
*/
@Deprecated
public PDFImageXObject getImage(String key) {
return (PDFImageXObject)getXObject(key);
}
/**
* Get an XObject from the image map.
*
* @param key the XObject key to look for
* @return the PDFXObject for the key if found
*/
public PDFXObject getXObject(String key) {
Object xObj = xObjectsMapFast.get(key);
if (xObj != null) {
return (PDFXObject) xObj;
}
return xObjectsMap.get(toHashCode(key));
}
private void putXObject(String key, PDFXObject pdfxObject) {
xObjectsMapFast.clear();
xObjectsMapFast.put(key, pdfxObject);
xObjectsMap.put(toHashCode(key), pdfxObject);
}
private String toHashCode(String key) {
if (key.length() < 1024) {
return key;
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] thedigest = md.digest(key.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : thedigest) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Adds a destination to the document.
* @param destination the destination object
*/
public void addDestination(PDFDestination destination) {
if (this.destinations == null) {
this.destinations = new ArrayList<PDFDestination>();
}
this.destinations.add(destination);
}
/**
* Gets the list of named destinations.
*
* @return the list of named destinations.
*/
public List<PDFDestination> getDestinationList() {
if (hasDestinations()) {
return this.destinations;
} else {
return Collections.emptyList();
}
}
/**
* Gets whether the document has named destinations.
*
* @return whether the document has named destinations.
*/
public boolean hasDestinations() {
return this.destinations != null && !this.destinations.isEmpty();
}
/**
* 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 {@link 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 PDFImageXObject addImage(PDFResourceContext res, PDFImage img) {
// check if already created
String key = img.getKey();
PDFImageXObject xObject = (PDFImageXObject)getXObject(key);
if (xObject != null) {
if (res != null) {
res.addXObject(xObject);
}
return xObject;
}
// setup image
img.setup(this);
// create a new XObject
xObject = new PDFImageXObject(++this.xObjectCount, img);
registerObject(xObject);
this.resources.addXObject(xObject);
if (res != null) {
res.addXObject(xObject);
}
putXObject(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 {@link 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 a reference to 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,
PDFReference formres,
String key) {
// check if already created
PDFFormXObject xObject = (PDFFormXObject)getXObject(key);
if (xObject != null) {
if (res != null) {
res.addXObject(xObject);
}
return xObject;
}
xObject = new PDFFormXObject(
++this.xObjectCount,
cont,
formres);
registerObject(xObject);
this.resources.addXObject(xObject);
if (res != null) {
res.addXObject(xObject);
}
putXObject(key, 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 (this.outlineRoot != null) {
return this.outlineRoot;
}
this.outlineRoot = new PDFOutline(null, null, true);
assignObjectNumber(this.outlineRoot);
addTrailerObject(this.outlineRoot);
this.root.setRootOutline(this.outlineRoot);
return this.outlineRoot;
}
/**
* Get the /Resources object for the document
*
* @return the /Resources object
*/
public PDFResources getResources() {
return this.resources;
}
public void enableAccessibility(boolean enableAccessibility) {
this.accessibilityEnabled = enableAccessibility;
}
/**
*
*/
public PDFReference resolveExtensionReference(String id) {
if (layers != null) {
for (PDFLayer layer : layers) {
if (layer.hasId(id)) {
return layer.makeReference();
}
}
}
if (navigators != null) {
for (PDFNavigator navigator : navigators) {
if (navigator.hasId(id)) {
return navigator.makeReference();
}
}
}
if (navigatorActions != null) {
for (PDFNavigatorAction action : navigatorActions) {
if (action.hasId(id)) {
return action.makeReference();
}
}
}
return null;
}
/**
* Writes out the entire document
*
* @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 {
outputStarted = true;
//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) {
PDFObject object = this.objects.remove(0);
streamIndirectObject(object, stream);
}
}
protected void writeTrailer(OutputStream stream, int first, int last, int size, long mainOffset, long startxref)
throws IOException {
TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements()
? new CompressedTrailerOutputHelper()
: new UncompressedTrailerOutputHelper();
if (structureTreeElements != null) {
trailerOutputHelper.outputStructureTreeElements(stream);
}
TrailerDictionary trailerDictionary = createTrailerDictionary(mainOffset != 0);
if (mainOffset != 0) {
trailerDictionary.getDictionary().put("Prev", mainOffset);
}
trailerOutputHelper.outputCrossReferenceObject(stream, trailerDictionary, first, last, size);
String trailer = "\nstartxref\n" + startxref + "\n%%EOF\n";
stream.write(encode(trailer));
}
protected int streamIndirectObject(PDFObject o, OutputStream stream) throws IOException {
outputStarted = true;
recordObjectOffset(o);
int len = outputIndirectObject(o, stream);
this.position += len;
return len;
}
private void streamIndirectObjects(Collection<? extends PDFObject> objects, OutputStream stream)
throws IOException {
for (PDFObject o : objects) {
streamIndirectObject(o, stream);
}
}
private void recordObjectOffset(PDFObject object) {
int index = object.getObjectNumber().getNumber() - 1;
while (indirectObjectOffsets.size() <= index) {
indirectObjectOffsets.add(null);
}
indirectObjectOffsets.set(index, position);
}
/**
* Outputs the given object, wrapped by obj/endobj, to the given stream.
*
* @param object an indirect object, as described in Section 3.2.9 of the PDF 1.5
* Reference.
* @param stream the stream to which the object must be output
* @throws IllegalArgumentException if the object is not an indirect object
*/
public static int outputIndirectObject(PDFObject object, OutputStream stream)
throws IOException {
if (!object.hasObjectNumber()) {
throw new IllegalArgumentException("Not an indirect object");
}
byte[] obj = encode(object.getObjectID());
stream.write(obj);
int length = object.output(stream);
byte[] endobj = encode("\nendobj\n");
stream.write(endobj);
return obj.length + length + endobj.length;
}
/**
* 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;
getProfile().verifyPDFVersion();
byte[] pdf = encode("%PDF-" + getPDFVersionString() + "\n");
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 {
createDestinations();
output(stream);
outputTrailerObjectsAndXref(stream);
}
private void createDestinations() {
if (hasDestinations()) {
Collections.sort(this.destinations, new DestinationComparator());
PDFDests dests = getFactory().makeDests(this.destinations);
if (this.root.getNames() == null) {
this.root.setNames(getFactory().makeNames());
}
this.root.getNames().setDests(dests);
}
}
private void outputTrailerObjectsAndXref(OutputStream stream) throws IOException {
TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements()
? new CompressedTrailerOutputHelper()
: new UncompressedTrailerOutputHelper();
if (structureTreeElements != null) {
trailerOutputHelper.outputStructureTreeElements(stream);
}
streamIndirectObjects(trailerObjects, stream);
TrailerDictionary trailerDictionary = createTrailerDictionary(true);
long startxref = trailerOutputHelper.outputCrossReferenceObject(stream, trailerDictionary, 0,
indirectObjectOffsets.size(), indirectObjectOffsets.size());
String trailer = "\nstartxref\n" + startxref + "\n%%EOF\n";
stream.write(encode(trailer));
}
private boolean mayCompressStructureTreeElements() {
return accessibilityEnabled
&& versionController.getPDFVersion().compareTo(Version.V1_5) >= 0
&& !isLinearizationEnabled();
}
private TrailerDictionary createTrailerDictionary(boolean addRoot) {
FileIDGenerator gen = getFileIDGenerator();
TrailerDictionary trailerDictionary = new TrailerDictionary(this);
if (addRoot) {
trailerDictionary.setRoot(root).setInfo(info);
}
trailerDictionary.setFileID(gen.getOriginalFileID(), gen.getUpdatedFileID());
if (isEncryptionActive()) {
trailerDictionary.setEncryption(encryption);
}
return trailerDictionary;
}
public boolean isMergeFontsEnabled() {
return mergeFontsEnabled;
}
public void setMergeFontsEnabled(boolean mergeFontsEnabled) {
this.mergeFontsEnabled = mergeFontsEnabled;
if (mergeFontsEnabled) {
getResources().createFontsAsObj();
}
}
private interface TrailerOutputHelper {
void outputStructureTreeElements(OutputStream stream) throws IOException;
/**
* @return the offset of the cross-reference object (the value of startxref)
*/
long outputCrossReferenceObject(OutputStream stream, TrailerDictionary trailerDictionary,
int first, int last, int size)
throws IOException;
}
private class UncompressedTrailerOutputHelper implements TrailerOutputHelper {
public void outputStructureTreeElements(OutputStream stream)
throws IOException {
streamIndirectObjects(structureTreeElements, stream);
}
public long outputCrossReferenceObject(OutputStream stream,
TrailerDictionary trailerDictionary, int first, int last, int size) throws IOException {
new CrossReferenceTable(trailerDictionary, position,
indirectObjectOffsets, first, last, size).output(stream);
return position;
}
}
private class CompressedTrailerOutputHelper implements TrailerOutputHelper {
private ObjectStreamManager structureTreeObjectStreams;
public void outputStructureTreeElements(OutputStream stream)
throws IOException {
assert structureTreeElements.size() > 0;
structureTreeObjectStreams = new ObjectStreamManager(PDFDocument.this);
for (PDFStructElem structElem : structureTreeElements) {
structureTreeObjectStreams.add(structElem);
}
}
public long outputCrossReferenceObject(OutputStream stream,
TrailerDictionary trailerDictionary, int first, int last, int size) throws IOException {
// Outputting the object streams should not have created new indirect objects
assert objects.isEmpty();
new CrossReferenceStream(PDFDocument.this, ++objectcount, trailerDictionary, position,
indirectObjectOffsets,
structureTreeObjectStreams.getCompressedObjectReferences())
.output(stream);
return position;
}
}
long getCurrentFileSize() {
return position;
}
FileIDGenerator getFileIDGenerator() {
if (fileIDGenerator == null) {
try {
fileIDGenerator = FileIDGenerator.getDigestFileIDGenerator(this);
} catch (NoSuchAlgorithmException e) {
fileIDGenerator = FileIDGenerator.getRandomFileIDGenerator();
}
}
return fileIDGenerator;
}
public boolean isLinearizationEnabled() {
return linearizationEnabled;
}
public void setLinearizationEnabled(boolean b) {
linearizationEnabled = b;
}
public boolean isFormXObjectEnabled() {
return formXObjectEnabled;
}
public void setFormXObjectEnabled(boolean b) {
formXObjectEnabled = b;
}
}