com.itextpdf.text.pdf.PdfWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.itextpdf.text.pdf.PdfWriter.java

Source

/*
 *
 * This file is part of the iText (R) project.
Copyright (c) 1998-2019 iText Group NV
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: sales@itextpdf.com
 */
package com.itextpdf.text.pdf;

import com.itextpdf.text.*;
import com.itextpdf.text.error_messages.MessageLocalization;
import com.itextpdf.text.io.TempFileCache;
import com.itextpdf.text.log.Counter;
import com.itextpdf.text.log.CounterFactory;
import com.itextpdf.text.pdf.collection.PdfCollection;
import com.itextpdf.text.pdf.events.PdfPageEventForwarder;
import com.itextpdf.text.pdf.interfaces.*;
import com.itextpdf.text.pdf.internal.PdfIsoKeys;
import com.itextpdf.text.pdf.internal.PdfVersionImp;
import com.itextpdf.text.pdf.internal.PdfXConformanceImp;
import com.itextpdf.text.xml.xmp.PdfProperties;
import com.itextpdf.text.xml.xmp.XmpWriter;
import com.itextpdf.xmp.XMPConst;
import com.itextpdf.xmp.XMPException;
import com.itextpdf.xmp.options.PropertyOptions;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.cert.Certificate;
import java.util.*;
import java.util.List;

/**
 * A <CODE>DocWriter</CODE> class for PDF.
 * <P>
 * When this <CODE>PdfWriter</CODE> is added
 * to a certain <CODE>PdfDocument</CODE>, the PDF representation of every Element
 * added to this Document will be written to the outputstream.</P>
 */

public class PdfWriter extends DocWriter implements PdfViewerPreferences, PdfEncryptionSettings, PdfVersion,
        PdfDocumentActions, PdfPageActions, PdfRunDirection, PdfAnnotations {

    /**
     * The highest generation number possible.
     * @since   iText 2.1.6
     */
    public static final int GENERATION_MAX = 65535;

    // INNER CLASSES

    /**
     * This class generates the structure of a PDF document.
     * <P>
     * This class covers the third section of Chapter 5 in the 'Portable Document Format
     * Reference Manual version 1.3' (page 55-60). It contains the body of a PDF document
     * (section 5.14) and it can also generate a Cross-reference Table (section 5.15).
     *
     * @see      PdfWriter
     * @see      PdfObject
     * @see      PdfIndirectObject
     */

    public static class PdfBody {

        // inner classes

        /**
         * <CODE>PdfCrossReference</CODE> is an entry in the PDF Cross-Reference table.
         */

        static public class PdfCrossReference implements Comparable<PdfCrossReference> {

            // membervariables
            private final int type;

            /**   Byte offset in the PDF file. */
            private final long offset;

            private final int refnum;
            /**   generation of the object. */
            private final int generation;

            // constructors
            /**
             * Constructs a cross-reference element for a PdfIndirectObject.
             * @param refnum
             * @param   offset      byte offset of the object
             * @param   generation   generation number of the object
             */

            public PdfCrossReference(final int refnum, final long offset, final int generation) {
                type = 0;
                this.offset = offset;
                this.refnum = refnum;
                this.generation = generation;
            }

            /**
             * Constructs a cross-reference element for a PdfIndirectObject.
             * @param refnum
             * @param   offset      byte offset of the object
             */

            public PdfCrossReference(final int refnum, final long offset) {
                type = 1;
                this.offset = offset;
                this.refnum = refnum;
                this.generation = 0;
            }

            public PdfCrossReference(final int type, final int refnum, final long offset, final int generation) {
                this.type = type;
                this.offset = offset;
                this.refnum = refnum;
                this.generation = generation;
            }

            public int getRefnum() {
                return refnum;
            }

            /**
             * Returns the PDF representation of this <CODE>PdfObject</CODE>.
             * @param os
             * @throws IOException
             */

            public void toPdf(final OutputStream os) throws IOException {
                StringBuffer off = new StringBuffer("0000000000").append(offset);
                off.delete(0, off.length() - 10);
                StringBuffer gen = new StringBuffer("00000").append(generation);
                gen.delete(0, gen.length() - 5);

                off.append(' ').append(gen).append(generation == GENERATION_MAX ? " f \n" : " n \n");
                os.write(getISOBytes(off.toString()));
            }

            /**
             * Writes PDF syntax to the OutputStream
             * @param midSize
             * @param os
             * @throws IOException
             */
            public void toPdf(int midSize, final OutputStream os) throws IOException {
                os.write((byte) type);
                while (--midSize >= 0)
                    os.write((byte) (offset >>> 8 * midSize & 0xff));
                os.write((byte) (generation >>> 8 & 0xff));
                os.write((byte) (generation & 0xff));
            }

            /**
             * @see java.lang.Comparable#compareTo(java.lang.Object)
             */
            public int compareTo(final PdfCrossReference other) {
                return refnum < other.refnum ? -1 : refnum == other.refnum ? 0 : 1;
            }

            /**
             * @see java.lang.Object#equals(java.lang.Object)
             */
            @Override
            public boolean equals(final Object obj) {
                if (obj instanceof PdfCrossReference) {
                    PdfCrossReference other = (PdfCrossReference) obj;
                    return refnum == other.refnum;
                } else
                    return false;
            }

            /**
             * @see java.lang.Object#hashCode()
             */
            @Override
            public int hashCode() {
                return refnum;
            }

        }

        private static final int OBJSINSTREAM = 200;

        // membervariables

        /** array containing the cross-reference table of the normal objects. */
        protected final TreeSet<PdfCrossReference> xrefs;
        protected int refnum;
        /** the current byte position in the body. */
        protected long position;
        protected final PdfWriter writer;
        protected ByteBuffer index;
        protected ByteBuffer streamObjects;
        protected int currentObjNum;
        protected int numObj = 0;

        // constructors

        /**
         * Constructs a new <CODE>PdfBody</CODE>.
         * @param writer
         */
        protected PdfBody(final PdfWriter writer) {
            xrefs = new TreeSet<PdfCrossReference>();
            xrefs.add(new PdfCrossReference(0, 0, GENERATION_MAX));
            position = writer.getOs().getCounter();
            refnum = 1;
            this.writer = writer;
        }

        // methods

        void setRefnum(final int refnum) {
            this.refnum = refnum;
        }

        protected PdfWriter.PdfBody.PdfCrossReference addToObjStm(final PdfObject obj, final int nObj)
                throws IOException {
            if (numObj >= OBJSINSTREAM)
                flushObjStm();
            if (index == null) {
                index = new ByteBuffer();
                streamObjects = new ByteBuffer();
                currentObjNum = getIndirectReferenceNumber();
                numObj = 0;
            }
            int p = streamObjects.size();
            int idx = numObj++;
            PdfEncryption enc = writer.crypto;
            writer.crypto = null;
            obj.toPdf(writer, streamObjects);
            writer.crypto = enc;
            streamObjects.append(' ');
            index.append(nObj).append(' ').append(p).append(' ');
            return new PdfWriter.PdfBody.PdfCrossReference(2, nObj, currentObjNum, idx);
        }

        public void flushObjStm() throws IOException {
            if (numObj == 0)
                return;
            int first = index.size();
            index.append(streamObjects);
            PdfStream stream = new PdfStream(index.toByteArray());
            stream.flateCompress(writer.getCompressionLevel());
            stream.put(PdfName.TYPE, PdfName.OBJSTM);
            stream.put(PdfName.N, new PdfNumber(numObj));
            stream.put(PdfName.FIRST, new PdfNumber(first));
            add(stream, currentObjNum);
            index = null;
            streamObjects = null;
            numObj = 0;
        }

        /**
         * Adds a <CODE>PdfObject</CODE> to the body.
         * <P>
         * This methods creates a <CODE>PdfIndirectObject</CODE> with a
         * certain number, containing the given <CODE>PdfObject</CODE>.
         * It also adds a <CODE>PdfCrossReference</CODE> for this object
         * to an <CODE>ArrayList</CODE> that will be used to build the
         * Cross-reference Table.
         *
         * @param      object         a <CODE>PdfObject</CODE>
         * @return      a <CODE>PdfIndirectObject</CODE>
         * @throws IOException
         */

        PdfIndirectObject add(final PdfObject object) throws IOException {
            return add(object, getIndirectReferenceNumber());
        }

        PdfIndirectObject add(final PdfObject object, final boolean inObjStm) throws IOException {
            return add(object, getIndirectReferenceNumber(), 0, inObjStm);
        }

        /**
         * Gets a PdfIndirectReference for an object that will be created in the future.
         * @return a PdfIndirectReference
         */

        public PdfIndirectReference getPdfIndirectReference() {
            return new PdfIndirectReference(0, getIndirectReferenceNumber());
        }

        protected int getIndirectReferenceNumber() {
            int n = refnum++;
            xrefs.add(new PdfCrossReference(n, 0, GENERATION_MAX));
            return n;
        }

        /**
         * Adds a <CODE>PdfObject</CODE> to the body given an already existing
         * PdfIndirectReference.
         * <P>
         * This methods creates a <CODE>PdfIndirectObject</CODE> with the number given by
         * <CODE>ref</CODE>, containing the given <CODE>PdfObject</CODE>.
         * It also adds a <CODE>PdfCrossReference</CODE> for this object
         * to an <CODE>ArrayList</CODE> that will be used to build the
         * Cross-reference Table.
         *
         * @param      object         a <CODE>PdfObject</CODE>
         * @param      ref              a <CODE>PdfIndirectReference</CODE>
         * @return      a <CODE>PdfIndirectObject</CODE>
         * @throws IOException
         */

        PdfIndirectObject add(final PdfObject object, final PdfIndirectReference ref) throws IOException {
            return add(object, ref, true);
        }

        PdfIndirectObject add(final PdfObject object, final PdfIndirectReference ref, final boolean inObjStm)
                throws IOException {
            return add(object, ref.getNumber(), ref.getGeneration(), inObjStm);
        }

        PdfIndirectObject add(final PdfObject object, final int refNumber) throws IOException {
            return add(object, refNumber, 0, true); // to false
        }

        protected PdfIndirectObject add(final PdfObject object, final int refNumber, final int generation,
                final boolean inObjStm) throws IOException {
            if (inObjStm && object.canBeInObjStm() && writer.isFullCompression()) {
                PdfCrossReference pxref = addToObjStm(object, refNumber);
                PdfIndirectObject indirect = new PdfIndirectObject(refNumber, object, writer);
                if (!xrefs.add(pxref)) {
                    xrefs.remove(pxref);
                    xrefs.add(pxref);
                }
                return indirect;
            } else {
                PdfIndirectObject indirect;
                if (writer.isFullCompression()) {
                    indirect = new PdfIndirectObject(refNumber, object, writer);
                    write(indirect, refNumber);
                } else {
                    indirect = new PdfIndirectObject(refNumber, generation, object, writer);
                    write(indirect, refNumber, generation);
                }
                return indirect;
            }
        }

        protected void write(final PdfIndirectObject indirect, final int refNumber) throws IOException {
            PdfCrossReference pxref = new PdfCrossReference(refNumber, position);
            if (!xrefs.add(pxref)) {
                xrefs.remove(pxref);
                xrefs.add(pxref);
            }
            indirect.writeTo(writer.getOs());
            position = writer.getOs().getCounter();
        }

        protected void write(final PdfIndirectObject indirect, final int refNumber, final int generation)
                throws IOException {
            PdfCrossReference pxref = new PdfCrossReference(refNumber, position, generation);
            if (!xrefs.add(pxref)) {
                xrefs.remove(pxref);
                xrefs.add(pxref);
            }
            indirect.writeTo(writer.getOs());
            position = writer.getOs().getCounter();
        }

        /**
         * Returns the offset of the Cross-Reference table.
         *
         * @return      an offset
         */

        public long offset() {
            return position;
        }

        /**
         * Returns the total number of objects contained in the CrossReferenceTable of this <CODE>Body</CODE>.
         *
         * @return   a number of objects
         */

        public int size() {
            return Math.max(xrefs.last().getRefnum() + 1, refnum);
        }

        /**
         * Returns the CrossReferenceTable of the <CODE>Body</CODE>.
         * @param os
         * @param root
         * @param info
         * @param encryption
         * @param fileID
         * @param prevxref
         * @throws IOException
         */

        public void writeCrossReferenceTable(final OutputStream os, final PdfIndirectReference root,
                final PdfIndirectReference info, final PdfIndirectReference encryption, final PdfObject fileID,
                final long prevxref) throws IOException {
            int refNumber = 0;
            if (writer.isFullCompression()) {
                flushObjStm();
                refNumber = getIndirectReferenceNumber();
                xrefs.add(new PdfCrossReference(refNumber, position));
            }
            PdfCrossReference entry = xrefs.first();
            int first = entry.getRefnum();
            int len = 0;
            ArrayList<Integer> sections = new ArrayList<Integer>();
            for (PdfCrossReference pdfCrossReference : xrefs) {
                entry = pdfCrossReference;
                if (first + len == entry.getRefnum())
                    ++len;
                else {
                    sections.add(Integer.valueOf(first));
                    sections.add(Integer.valueOf(len));
                    first = entry.getRefnum();
                    len = 1;
                }
            }
            sections.add(Integer.valueOf(first));
            sections.add(Integer.valueOf(len));
            if (writer.isFullCompression()) {
                int mid = 5;
                long mask = 0xff00000000L;
                for (; mid > 1; --mid) {
                    if ((mask & position) != 0)
                        break;
                    mask >>>= 8;
                }
                ByteBuffer buf = new ByteBuffer();

                for (Object element : xrefs) {
                    entry = (PdfCrossReference) element;
                    entry.toPdf(mid, buf);
                }
                PdfStream xr = new PdfStream(buf.toByteArray());
                buf = null;
                xr.flateCompress(writer.getCompressionLevel());
                xr.put(PdfName.SIZE, new PdfNumber(size()));
                xr.put(PdfName.ROOT, root);
                if (info != null) {
                    xr.put(PdfName.INFO, info);
                }
                if (encryption != null)
                    xr.put(PdfName.ENCRYPT, encryption);
                if (fileID != null)
                    xr.put(PdfName.ID, fileID);
                xr.put(PdfName.W, new PdfArray(new int[] { 1, mid, 2 }));
                xr.put(PdfName.TYPE, PdfName.XREF);
                PdfArray idx = new PdfArray();
                for (int k = 0; k < sections.size(); ++k)
                    idx.add(new PdfNumber(sections.get(k).intValue()));
                xr.put(PdfName.INDEX, idx);
                if (prevxref > 0)
                    xr.put(PdfName.PREV, new PdfNumber(prevxref));
                PdfEncryption enc = writer.crypto;
                writer.crypto = null;
                PdfIndirectObject indirect = new PdfIndirectObject(refNumber, xr, writer);
                indirect.writeTo(writer.getOs());
                writer.crypto = enc;
            } else {
                os.write(getISOBytes("xref\n"));
                Iterator<PdfCrossReference> i = xrefs.iterator();
                for (int k = 0; k < sections.size(); k += 2) {
                    first = sections.get(k).intValue();
                    len = sections.get(k + 1).intValue();
                    os.write(getISOBytes(String.valueOf(first)));
                    os.write(getISOBytes(" "));
                    os.write(getISOBytes(String.valueOf(len)));
                    os.write('\n');
                    while (len-- > 0) {
                        entry = i.next();
                        entry.toPdf(os);
                    }
                }
            }
        }
    }

    /**
     * <CODE>PdfTrailer</CODE> is the PDF Trailer object.
     * <P>
     * This object is described in the 'Portable Document Format Reference Manual version 1.3'
     * section 5.16 (page 59-60).
     */

    static public class PdfTrailer extends PdfDictionary {

        // membervariables

        long offset;

        // constructors

        /**
         * Constructs a PDF-Trailer.
         *
         * @param      size      the number of entries in the <CODE>PdfCrossReferenceTable</CODE>
         * @param      offset      offset of the <CODE>PdfCrossReferenceTable</CODE>
         * @param      root      an indirect reference to the root of the PDF document
         * @param      info      an indirect reference to the info object of the PDF document
         * @param encryption
         * @param fileID
         * @param prevxref
         */

        public PdfTrailer(final int size, final long offset, final PdfIndirectReference root,
                final PdfIndirectReference info, final PdfIndirectReference encryption, final PdfObject fileID,
                final long prevxref) {
            this.offset = offset;
            put(PdfName.SIZE, new PdfNumber(size));
            put(PdfName.ROOT, root);
            if (info != null) {
                put(PdfName.INFO, info);
            }
            if (encryption != null)
                put(PdfName.ENCRYPT, encryption);
            if (fileID != null)
                put(PdfName.ID, fileID);
            if (prevxref > 0)
                put(PdfName.PREV, new PdfNumber(prevxref));
        }

        /**
         * Returns the PDF representation of this <CODE>PdfObject</CODE>.
         * @param writer
         * @param os
         * @throws IOException
         */
        @Override
        public void toPdf(final PdfWriter writer, final OutputStream os) throws IOException {
            PdfWriter.checkPdfIsoConformance(writer, PdfIsoKeys.PDFISOKEY_TRAILER, this);
            os.write(getISOBytes("trailer\n"));
            super.toPdf(null, os);
            os.write('\n');
            writeKeyInfo(os);
            os.write(getISOBytes("startxref\n"));
            os.write(getISOBytes(String.valueOf(offset)));
            os.write(getISOBytes("\n%%EOF\n"));
        }
    }

    //   ESSENTIALS
    protected static Counter COUNTER = CounterFactory.getCounter(PdfWriter.class);

    protected Counter getCounter() {
        return COUNTER;
    }

    //   Construct a PdfWriter instance

    /**
     * Constructs a <CODE>PdfWriter</CODE>.
     */
    protected PdfWriter() {
    }

    /**
     * Constructs a <CODE>PdfWriter</CODE>.
     * <P>
     * Remark: a PdfWriter can only be constructed by calling the method
     * <CODE>getInstance(Document document, OutputStream os)</CODE>.
     *
     * @param   document   The <CODE>PdfDocument</CODE> that has to be written
     * @param   os         The <CODE>OutputStream</CODE> the writer has to write to.
     */

    protected PdfWriter(final PdfDocument document, final OutputStream os) {
        super(document, os);
        pdf = document;
        directContentUnder = new PdfContentByte(this);
        directContent = directContentUnder.getDuplicate();
    }

    /**
     * Use this method to get an instance of the <CODE>PdfWriter</CODE>.
     *
     * @param   document   The <CODE>Document</CODE> that has to be written
     * @param   os   The <CODE>OutputStream</CODE> the writer has to write to.
     * @return   a new <CODE>PdfWriter</CODE>
     *
     * @throws   DocumentException on error
     */

    public static PdfWriter getInstance(final Document document, final OutputStream os) throws DocumentException {
        PdfDocument pdf = new PdfDocument();
        document.addDocListener(pdf);
        PdfWriter writer = new PdfWriter(pdf, os);
        pdf.addWriter(writer);
        return writer;
    }

    /**
     * Use this method to get an instance of the <CODE>PdfWriter</CODE>.
     *
     * @return a new <CODE>PdfWriter</CODE>
     * @param document The <CODE>Document</CODE> that has to be written
     * @param os The <CODE>OutputStream</CODE> the writer has to write to.
     * @param listener A <CODE>DocListener</CODE> to pass to the PdfDocument.
     * @throws DocumentException on error
     */

    public static PdfWriter getInstance(final Document document, final OutputStream os, final DocListener listener)
            throws DocumentException {
        PdfDocument pdf = new PdfDocument();
        pdf.addDocListener(listener);
        document.addDocListener(pdf);
        PdfWriter writer = new PdfWriter(pdf, os);
        pdf.addWriter(writer);
        return writer;
    }

    //   the PdfDocument instance

    /** the pdfdocument object. */
    protected PdfDocument pdf;

    /**
     * Gets the <CODE>PdfDocument</CODE> associated with this writer.
     * @return the <CODE>PdfDocument</CODE>
     */

    PdfDocument getPdfDocument() {
        return pdf;
    }

    /**
     * Use this method to get the info dictionary if you want to
     * change it directly (add keys and values to the info dictionary).
     * @return the info dictionary
     */
    public PdfDictionary getInfo() {
        return pdf.getInfo();
    }

    /**
     * Use this method to get the current vertical page position.
     * @param ensureNewLine Tells whether a new line shall be enforced. This may cause side effects
     *   for elements that do not terminate the lines they've started because those lines will get
     *   terminated.
     * @return The current vertical page position.
     */
    public float getVerticalPosition(final boolean ensureNewLine) {
        return pdf.getVerticalPosition(ensureNewLine);
    }

    /**
     * Sets the initial leading for the PDF document.
     * This has to be done before the document is opened.
     * @param   leading   the initial leading
     * @since   2.1.6
     * @throws   DocumentException   if you try setting the leading after the document was opened.
     */
    public void setInitialLeading(final float leading) throws DocumentException {
        if (open)
            throw new DocumentException(MessageLocalization
                    .getComposedMessage("you.can.t.set.the.initial.leading.if.the.document.is.already.open"));
        pdf.setLeading(leading);
    }

    //   the PdfDirectContentByte instances

    /*
     * You should see Direct Content as a canvas on which you can draw
     * graphics and text. One canvas goes on top of the page (getDirectContent),
     * the other goes underneath (getDirectContentUnder).
     * You can always the same object throughout your document,
     * even if you have moved to a new page. Whatever you add on
     * the canvas will be displayed on top or under the current page.
     */

    /** The direct content in this document. */
    protected PdfContentByte directContent;

    /** The direct content under in this document. */
    protected PdfContentByte directContentUnder;

    /**
     * Use this method to get the direct content for this document.
     * There is only one direct content, multiple calls to this method
     * will allways retrieve the same object.
     * @return the direct content
     */

    public PdfContentByte getDirectContent() {
        if (!open)
            throw new RuntimeException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
        return directContent;
    }

    /**
     * Use this method to get the direct content under for this document.
     * There is only one direct content, multiple calls to this method
     * will always retrieve the same object.
     * @return the direct content
     */

    public PdfContentByte getDirectContentUnder() {
        if (!open)
            throw new RuntimeException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
        return directContentUnder;
    }

    /**
     * Resets all the direct contents to empty.
     * This happens when a new page is started.
     */
    void resetContent() {
        directContent.reset();
        directContentUnder.reset();
    }

    //   PDF body

    /*
     * A PDF file has 4 parts: a header, a body, a cross-reference table, and a trailer.
     * The body contains all the PDF objects that make up the PDF document.
     * Each element gets a reference (a set of numbers) and the byte position of
     * every object is stored in the cross-reference table.
     * Use these methods only if you know what you're doing.
     */

    /** body of the PDF document */
    protected PdfBody body;

    protected ICC_Profile colorProfile;

    public ICC_Profile getColorProfile() {
        return colorProfile;
    }

    /**
     * Adds the local destinations to the body of the document.
     * @param desto the <CODE>HashMap</CODE> containing the destinations
     * @throws IOException on error
     */

    void addLocalDestinations(final TreeMap<String, PdfDocument.Destination> desto) throws IOException {
        for (Map.Entry<String, PdfDocument.Destination> entry : desto.entrySet()) {
            String name = entry.getKey();
            PdfDocument.Destination dest = entry.getValue();
            PdfDestination destination = dest.destination;
            if (dest.reference == null)
                dest.reference = getPdfIndirectReference();
            if (destination == null)
                addToBody(new PdfString("invalid_" + name), dest.reference);
            else
                addToBody(destination, dest.reference);
        }
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object) throws IOException {
        PdfIndirectObject iobj = body.add(object);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @param inObjStm
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object, final boolean inObjStm) throws IOException {
        PdfIndirectObject iobj = body.add(object, inObjStm);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @param ref
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object, final PdfIndirectReference ref) throws IOException {
        PdfIndirectObject iobj = body.add(object, ref);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @param ref
     * @param inObjStm
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object, final PdfIndirectReference ref,
            final boolean inObjStm) throws IOException {
        PdfIndirectObject iobj = body.add(object, ref, inObjStm);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @param refNumber
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object, final int refNumber) throws IOException {
        PdfIndirectObject iobj = body.add(object, refNumber);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method to add a PDF object to the PDF body.
     * Use this method only if you know what you're doing!
     * @param object
     * @param refNumber
     * @param inObjStm
     * @return a PdfIndirectObject
     * @throws IOException
     */
    public PdfIndirectObject addToBody(final PdfObject object, final int refNumber, final boolean inObjStm)
            throws IOException {
        PdfIndirectObject iobj = body.add(object, refNumber, 0, inObjStm);
        cacheObject(iobj);
        return iobj;
    }

    /**
     * Use this method for caching objects.
     * @param iobj @see PdfIndirectObject
     */
    protected void cacheObject(PdfIndirectObject iobj) {
    }

    /**
     * Use this to get an <CODE>PdfIndirectReference</CODE> for an object that
     * will be created in the future.
     * Use this method only if you know what you're doing!
     * @return the <CODE>PdfIndirectReference</CODE>
     */

    public PdfIndirectReference getPdfIndirectReference() {
        return body.getPdfIndirectReference();
    }

    protected int getIndirectReferenceNumber() {
        return body.getIndirectReferenceNumber();
    }

    /**
     * Returns the outputStreamCounter.
     * @return the outputStreamCounter
     */
    public OutputStreamCounter getOs() {
        return os;
    }

    //   PDF Catalog

    /*
     * The Catalog is also called the root object of the document.
     * Whereas the Cross-Reference maps the objects number with the
     * byte offset so that the viewer can find the objects, the
     * Catalog tells the viewer the numbers of the objects needed
     * to render the document.
     */

    protected PdfDictionary getCatalog(final PdfIndirectReference rootObj) {
        PdfDictionary catalog = pdf.getCatalog(rootObj);
        // [F12] tagged PDF
        buildStructTreeRootForTagged(catalog);
        // [F13] OCG
        if (!documentOCG.isEmpty()) {
            fillOCProperties(false);
            catalog.put(PdfName.OCPROPERTIES, OCProperties);
        }
        return catalog;
    }

    protected void buildStructTreeRootForTagged(PdfDictionary catalog) {
        if (tagged) {
            try {
                getStructureTreeRoot().buildTree();
                for (AccessibleElementId elementId : pdf.getStructElements()) {
                    PdfStructureElement element = pdf.getStructElement(elementId, false);
                    addToBody(element, element.getReference());
                }

            } catch (Exception e) {
                throw new ExceptionConverter(e);
            }
            catalog.put(PdfName.STRUCTTREEROOT, structureTreeRoot.getReference());
            PdfDictionary mi = new PdfDictionary();
            mi.put(PdfName.MARKED, PdfBoolean.PDFTRUE);
            if (userProperties)
                mi.put(PdfName.USERPROPERTIES, PdfBoolean.PDFTRUE);
            catalog.put(PdfName.MARKINFO, mi);
        }
    }

    /** Holds value of property extraCatalog this is used for Output Intents. */
    protected PdfDictionary extraCatalog;

    /**
     * Sets extra keys to the catalog.
     * @return the catalog to change
     */
    public PdfDictionary getExtraCatalog() {
        if (extraCatalog == null)
            extraCatalog = new PdfDictionary();
        return this.extraCatalog;
    }

    //   PdfPages

    /*
     * The page root keeps the complete page tree of the document.
     * There's an entry in the Catalog that refers to the root
     * of the page tree, the page tree contains the references
     * to pages and other page trees.
     */

    /** The root of the page tree. */
    protected PdfPages root = new PdfPages(this);
    /** The PdfIndirectReference to the pages. */
    protected ArrayList<PdfIndirectReference> pageReferences = new ArrayList<PdfIndirectReference>();
    /** The current page number. */
    protected int currentPageNumber = 1;
    /**
     * The value of the Tabs entry in the page dictionary.
     * @since   2.1.5
     */
    protected PdfName tabs = null;

    /**
     * Additional page dictionary entries.
     * @since 5.1.0
     */
    protected PdfDictionary pageDictEntries = new PdfDictionary();

    /**
     * Adds an additional entry for the page dictionary.
     * @param key the key
     * @param object the PdfObject for the given key
     * @since 5.1.0
     */
    public void addPageDictEntry(final PdfName key, final PdfObject object) {
        pageDictEntries.put(key, object);
    }

    /**
     * Gets the additional pageDictEntries.
     * @return the page dictionary entries
     * @since 5.1.0
     */
    public PdfDictionary getPageDictEntries() {
        return pageDictEntries;
    }

    /**
     * Resets the additional pageDictEntries.
     * @since 5.1.0
     */
    public void resetPageDictEntries() {
        pageDictEntries = new PdfDictionary();
    }

    /**
     * Use this method to make sure the page tree has a linear structure
     * (every leave is attached directly to the root).
     * Use this method to allow page reordering with method reorderPages.
     */
    public void setLinearPageMode() {
        root.setLinearMode(null);
    }

    /**
     * Use this method to reorder the pages in the document.
     * A <CODE>null</CODE> argument value only returns the number of pages to process.
     * It is advisable to issue a <CODE>Document.newPage()</CODE> before using this method.
     * @return the total number of pages
     * @param order an array with the new page sequence. It must have the
     * same size as the number of pages.
     * @throws DocumentException if all the pages are not present in the array
     */
    public int reorderPages(final int order[]) throws DocumentException {
        return root.reorderPages(order);
    }

    /**
     * Use this method to get a reference to a page existing or not.
     * If the page does not exist yet the reference will be created
     * in advance. If on closing the document, a page number greater
     * than the total number of pages was requested, an exception
     * is thrown.
     * @param page the page number. The first page is 1
     * @return the reference to the page
     */
    public PdfIndirectReference getPageReference(int page) {
        --page;
        if (page < 0)
            throw new IndexOutOfBoundsException(
                    MessageLocalization.getComposedMessage("the.page.number.must.be.gt.eq.1"));
        PdfIndirectReference ref;
        if (page < pageReferences.size()) {
            ref = pageReferences.get(page);
            if (ref == null) {
                ref = body.getPdfIndirectReference();
                pageReferences.set(page, ref);
            }
        } else {
            int empty = page - pageReferences.size();
            for (int k = 0; k < empty; ++k)
                pageReferences.add(null);
            ref = body.getPdfIndirectReference();
            pageReferences.add(ref);
        }
        return ref;
    }

    /**
     * Gets the pagenumber of this document.
     * This number can be different from the real pagenumber,
     * if you have (re)set the page number previously.
     * @return a page number
     */

    public int getPageNumber() {
        return pdf.getPageNumber();
    }

    PdfIndirectReference getCurrentPage() {
        return getPageReference(currentPageNumber);
    }

    public int getCurrentPageNumber() {
        return currentPageNumber;
    }

    /**
     * Sets the Viewport for the next page.
     * @param vp an array consisting of Viewport dictionaries.
     * @since 5.1.0
     */
    public void setPageViewport(final PdfArray vp) {
        addPageDictEntry(PdfName.VP, vp);
    }

    /**
     * Sets the value for the Tabs entry in the page tree.
     * @param   tabs   Can be PdfName.R, PdfName.C or PdfName.S.
     * Since the Adobe Extensions Level 3, it can also be PdfName.A
     * or PdfName.W
     * @since   2.1.5
     */
    public void setTabs(final PdfName tabs) {
        this.tabs = tabs;
    }

    /**
     * Returns the value to be used for the Tabs entry in the page tree.
     * @return the Tabs PdfName
     * @since   2.1.5
     */
    public PdfName getTabs() {
        return tabs;
    }

    /**
     * Adds some <CODE>PdfContents</CODE> to this Writer.
     * <P>
     * The document has to be open before you can begin to add content
     * to the body of the document.
     *
     * @return a <CODE>PdfIndirectReference</CODE>
     * @param page the <CODE>PdfPage</CODE> to add
     * @param contents the <CODE>PdfContents</CODE> of the page
     * @throws PdfException on error
     */

    PdfIndirectReference add(final PdfPage page, final PdfContents contents) throws PdfException {
        if (!open) {
            throw new PdfException(MessageLocalization.getComposedMessage("the.document.is.not.open"));
        }
        PdfIndirectObject object;
        try {
            object = addToBody(contents);
        } catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
        page.add(object.getIndirectReference());
        // [U5]
        if (group != null) {
            page.put(PdfName.GROUP, group);
            group = null;
        } else if (rgbTransparencyBlending) {
            PdfDictionary pp = new PdfDictionary();
            pp.put(PdfName.TYPE, PdfName.GROUP);
            pp.put(PdfName.S, PdfName.TRANSPARENCY);
            pp.put(PdfName.CS, PdfName.DEVICERGB);
            page.put(PdfName.GROUP, pp);
        }
        root.addPage(page);
        currentPageNumber++;
        return null;
    }

    //   page events

    /*
     * Page events are specific for iText, not for PDF.
     * Upon specific events (for instance when a page starts
     * or ends), the corresponding method in the page event
     * implementation that is added to the writer is invoked.
     */

    /** The <CODE>PdfPageEvent</CODE> for this document. */
    private PdfPageEvent pageEvent;

    /**
     * Sets the <CODE>PdfPageEvent</CODE> for this document.
     * @param event the <CODE>PdfPageEvent</CODE> for this document
     */

    public void setPageEvent(final PdfPageEvent event) {
        if (event == null)
            this.pageEvent = null;
        else if (this.pageEvent == null)
            this.pageEvent = event;
        else if (this.pageEvent instanceof PdfPageEventForwarder)
            ((PdfPageEventForwarder) this.pageEvent).addPageEvent(event);
        else {
            PdfPageEventForwarder forward = new PdfPageEventForwarder();
            forward.addPageEvent(this.pageEvent);
            forward.addPageEvent(event);
            this.pageEvent = forward;
        }
    }

    /**
     * Gets the <CODE>PdfPageEvent</CODE> for this document or <CODE>null</CODE>
     * if none is set.
     * @return the <CODE>PdfPageEvent</CODE> for this document or <CODE>null</CODE>
     * if none is set
     */

    public PdfPageEvent getPageEvent() {
        return pageEvent;
    }

    //   Open and Close methods + method that create the PDF

    /** A number referring to the previous Cross-Reference Table. */
    protected long prevxref = 0;
    /** The original file ID (if present). */
    protected byte[] originalFileID = null;

    /**
     * Signals that the <CODE>Document</CODE> has been opened and that
     * <CODE>Elements</CODE> can be added.
     * <P>
     * When this method is called, the PDF-document header is
     * written to the outputstream.
     * @see com.itextpdf.text.DocWriter#open()
     */
    @Override
    public void open() {
        super.open();
        try {
            pdf_version.writeHeader(os);
            body = new PdfBody(this);
            if (isPdfX() && ((PdfXConformanceImp) pdfIsoConformance).isPdfX32002()) {
                PdfDictionary sec = new PdfDictionary();
                sec.put(PdfName.GAMMA, new PdfArray(new float[] { 2.2f, 2.2f, 2.2f }));
                sec.put(PdfName.MATRIX, new PdfArray(new float[] { 0.4124f, 0.2126f, 0.0193f, 0.3576f, 0.7152f,
                        0.1192f, 0.1805f, 0.0722f, 0.9505f }));
                sec.put(PdfName.WHITEPOINT, new PdfArray(new float[] { 0.9505f, 1f, 1.089f }));
                PdfArray arr = new PdfArray(PdfName.CALRGB);
                arr.add(sec);
                setDefaultColorspace(PdfName.DEFAULTRGB, addToBody(arr).getIndirectReference());
            }
        } catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
    }

    /**
     * Signals that the <CODE>Document</CODE> was closed and that no other
     * <CODE>Elements</CODE> will be added.
     * <P>
     * The pages-tree is built and written to the outputstream.
     * A Catalog is constructed, as well as an Info-object,
     * the reference table is composed and everything is written
     * to the outputstream embedded in a Trailer.
     * @see com.itextpdf.text.DocWriter#close()
     */
    @Override
    public void close() {
        if (open) {
            if (currentPageNumber - 1 != pageReferences.size())
                throw new RuntimeException("The page " + pageReferences.size()
                        + " was requested but the document has only " + (currentPageNumber - 1) + " pages.");
            pdf.close();
            try {
                addSharedObjectsToBody();
                for (PdfOCG layer : documentOCG) {
                    addToBody(layer.getPdfObject(), layer.getRef());
                }
                // add the root to the body
                PdfIndirectReference rootRef = root.writePageTree();
                // make the catalog-object and add it to the body
                PdfDictionary catalog = getCatalog(rootRef);
                if (!documentOCG.isEmpty())
                    PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_LAYER, OCProperties);
                // [C9] if there is XMP data to add: add it
                if (xmpMetadata == null && xmpWriter != null) {
                    try {
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        xmpWriter.serialize(baos);
                        xmpWriter.close();
                        xmpMetadata = baos.toByteArray();
                    } catch (IOException exc) {
                        xmpWriter = null;
                    } catch (XMPException exc) {
                        xmpWriter = null;
                    }
                }
                if (xmpMetadata != null) {
                    PdfStream xmp = new PdfStream(xmpMetadata);
                    xmp.put(PdfName.TYPE, PdfName.METADATA);
                    xmp.put(PdfName.SUBTYPE, PdfName.XML);
                    if (crypto != null && !crypto.isMetadataEncrypted()) {
                        PdfArray ar = new PdfArray();
                        ar.add(PdfName.CRYPT);
                        xmp.put(PdfName.FILTER, ar);
                    }
                    catalog.put(PdfName.METADATA, body.add(xmp).getIndirectReference());
                }
                getInfo().put(PdfName.PRODUCER, new PdfString(Version.getInstance().getVersion()));
                // [C10] make pdfx conformant
                if (isPdfX()) {
                    completeInfoDictionary(getInfo());
                    completeExtraCatalog(getExtraCatalog());
                }
                // [C11] Output Intents
                if (extraCatalog != null) {
                    catalog.mergeDifferent(extraCatalog);
                }

                writeOutlines(catalog, false);

                // add the Catalog to the body
                PdfIndirectObject indirectCatalog = addToBody(catalog, false);
                // add the info-object to the body
                PdfIndirectObject infoObj = addToBody(getInfo(), false);

                // [F1] encryption
                PdfIndirectReference encryption = null;
                PdfObject fileID = null;
                body.flushObjStm();
                boolean isModified = (originalFileID != null);
                if (crypto != null) {
                    PdfIndirectObject encryptionObject = addToBody(crypto.getEncryptionDictionary(), false);
                    encryption = encryptionObject.getIndirectReference();
                    fileID = crypto.getFileID(isModified);
                } else {
                    fileID = PdfEncryption.createInfoId(
                            isModified ? originalFileID : PdfEncryption.createDocumentId(), isModified);
                }

                // write the cross-reference table of the body
                body.writeCrossReferenceTable(os, indirectCatalog.getIndirectReference(),
                        infoObj.getIndirectReference(), encryption, fileID, prevxref);

                // make the trailer
                // [F2] full compression
                if (fullCompression) {
                    writeKeyInfo(os);
                    os.write(getISOBytes("startxref\n"));
                    os.write(getISOBytes(String.valueOf(body.offset())));
                    os.write(getISOBytes("\n%%EOF\n"));
                } else {
                    PdfTrailer trailer = new PdfTrailer(body.size(), body.offset(),
                            indirectCatalog.getIndirectReference(), infoObj.getIndirectReference(), encryption,
                            fileID, prevxref);
                    trailer.toPdf(this, os);
                }
            } catch (IOException ioe) {
                throw new ExceptionConverter(ioe);
            } finally {
                super.close();
            }
        }
        getCounter().written(os.getCounter());
    }

    protected void addXFormsToBody() throws IOException {
        for (Object objs[] : formXObjects.values()) {
            PdfTemplate template = (PdfTemplate) objs[1];
            if (template != null && template.getIndirectReference() instanceof PRIndirectReference)
                continue;
            if (template != null && template.getType() == PdfTemplate.TYPE_TEMPLATE) {
                addToBody(template.getFormXObject(compressionLevel), template.getIndirectReference());
            }
        }
    }

    protected void addSharedObjectsToBody() throws IOException {
        // [F3] add the fonts
        for (FontDetails details : documentFonts.values()) {
            details.writeFont(this);
        }
        // [F4] add the form XObjects
        addXFormsToBody();
        // [F5] add all the dependencies in the imported pages
        for (PdfReaderInstance element : readerInstances.values()) {
            currentPdfReaderInstance = element;
            currentPdfReaderInstance.writeAllPages();
        }
        currentPdfReaderInstance = null;
        // [F6] add the spotcolors
        for (ColorDetails color : documentColors.values()) {
            addToBody(color.getPdfObject(this), color.getIndirectReference());
        }
        // [F7] add the pattern
        for (PdfPatternPainter pat : documentPatterns.keySet()) {
            addToBody(pat.getPattern(compressionLevel), pat.getIndirectReference());
        }
        // [F8] add the shading patterns
        for (PdfShadingPattern shadingPattern : documentShadingPatterns) {
            shadingPattern.addToBody();
        }
        // [F9] add the shadings
        for (PdfShading shading : documentShadings) {
            shading.addToBody();
        }
        // [F10] add the extgstate
        for (Map.Entry<PdfDictionary, PdfObject[]> entry : documentExtGState.entrySet()) {
            PdfDictionary gstate = entry.getKey();
            PdfObject obj[] = entry.getValue();
            addToBody(gstate, (PdfIndirectReference) obj[1]);
        }
        // [F11] add the properties
        for (Map.Entry<Object, PdfObject[]> entry : documentProperties.entrySet()) {
            Object prop = entry.getKey();
            PdfObject[] obj = entry.getValue();
            if (prop instanceof PdfLayerMembership) {
                PdfLayerMembership layer = (PdfLayerMembership) prop;
                addToBody(layer.getPdfObject(), layer.getRef());
            } else if (prop instanceof PdfDictionary && !(prop instanceof PdfLayer)) {
                addToBody((PdfDictionary) prop, (PdfIndirectReference) obj[1]);
            }
        }
    }

    // Root data for the PDF document (used when composing the Catalog)

    //  [C1] Outlines (bookmarks)

    /**
     * Use this method to get the root outline
     * and construct bookmarks.
     * @return the root outline
     */

    public PdfOutline getRootOutline() {
        return directContent.getRootOutline();
    }

    protected List<HashMap<String, Object>> newBookmarks;

    /**
     * Sets the bookmarks. The list structure is defined in
     * {@link SimpleBookmark}.
     * @param outlines the bookmarks or <CODE>null</CODE> to remove any
     */
    public void setOutlines(final List<HashMap<String, Object>> outlines) {
        newBookmarks = outlines;
    }

    protected void writeOutlines(final PdfDictionary catalog, final boolean namedAsNames) throws IOException {
        if (newBookmarks == null || newBookmarks.isEmpty())
            return;
        PdfDictionary top = new PdfDictionary();
        PdfIndirectReference topRef = getPdfIndirectReference();
        Object kids[] = SimpleBookmark.iterateOutlines(this, topRef, newBookmarks, namedAsNames);
        top.put(PdfName.FIRST, (PdfIndirectReference) kids[0]);
        top.put(PdfName.LAST, (PdfIndirectReference) kids[1]);
        top.put(PdfName.COUNT, new PdfNumber(((Integer) kids[2]).intValue()));
        addToBody(top, topRef);
        catalog.put(PdfName.OUTLINES, topRef);
    }

    //   [C2] PdfVersion interface
    /** possible PDF version (header) */
    public static final char VERSION_1_2 = '2';
    /** possible PDF version (header) */
    public static final char VERSION_1_3 = '3';
    /** possible PDF version (header) */
    public static final char VERSION_1_4 = '4';
    /** possible PDF version (header) */
    public static final char VERSION_1_5 = '5';
    /** possible PDF version (header) */
    public static final char VERSION_1_6 = '6';
    /** possible PDF version (header) */
    public static final char VERSION_1_7 = '7';

    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_2 = new PdfName("1.2");
    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_3 = new PdfName("1.3");
    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_4 = new PdfName("1.4");
    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_5 = new PdfName("1.5");
    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_6 = new PdfName("1.6");
    /** possible PDF version (catalog) */
    public static final PdfName PDF_VERSION_1_7 = new PdfName("1.7");

    /** Stores the version information for the header and the catalog. */
    protected PdfVersionImp pdf_version = new PdfVersionImp();

    /** @see com.itextpdf.text.pdf.interfaces.PdfVersion#setPdfVersion(char) */
    public void setPdfVersion(final char version) {
        pdf_version.setPdfVersion(version);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfVersion#setAtLeastPdfVersion(char) */
    public void setAtLeastPdfVersion(final char version) {
        pdf_version.setAtLeastPdfVersion(version);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfVersion#setPdfVersion(com.itextpdf.text.pdf.PdfName) */
    public void setPdfVersion(final PdfName version) {
        pdf_version.setPdfVersion(version);
    }

    /**
     * @see com.itextpdf.text.pdf.interfaces.PdfVersion#addDeveloperExtension(com.itextpdf.text.pdf.PdfDeveloperExtension)
     * @since   2.1.6
     */
    public void addDeveloperExtension(final PdfDeveloperExtension de) {
        pdf_version.addDeveloperExtension(de);
    }

    /**
     * Returns the version information.
     * @return the PdfVersion
     */
    PdfVersionImp getPdfVersion() {
        return pdf_version;
    }

    //  [C3] PdfViewerPreferences interface

    // page layout (section 13.1.1 of "iText in Action")

    /** A viewer preference */
    public static final int PageLayoutSinglePage = 1;
    /** A viewer preference */
    public static final int PageLayoutOneColumn = 2;
    /** A viewer preference */
    public static final int PageLayoutTwoColumnLeft = 4;
    /** A viewer preference */
    public static final int PageLayoutTwoColumnRight = 8;
    /** A viewer preference */
    public static final int PageLayoutTwoPageLeft = 16;
    /** A viewer preference */
    public static final int PageLayoutTwoPageRight = 32;

    // page mode (section 13.1.2 of "iText in Action")

    /** A viewer preference */
    public static final int PageModeUseNone = 64;
    /** A viewer preference */
    public static final int PageModeUseOutlines = 128;
    /** A viewer preference */
    public static final int PageModeUseThumbs = 256;
    /** A viewer preference */
    public static final int PageModeFullScreen = 512;
    /** A viewer preference */
    public static final int PageModeUseOC = 1024;
    /** A viewer preference */
    public static final int PageModeUseAttachments = 2048;

    // values for setting viewer preferences in iText versions older than 2.x

    /** A viewer preference */
    public static final int HideToolbar = 1 << 12;
    /** A viewer preference */
    public static final int HideMenubar = 1 << 13;
    /** A viewer preference */
    public static final int HideWindowUI = 1 << 14;
    /** A viewer preference */
    public static final int FitWindow = 1 << 15;
    /** A viewer preference */
    public static final int CenterWindow = 1 << 16;
    /** A viewer preference */
    public static final int DisplayDocTitle = 1 << 17;

    /** A viewer preference */
    public static final int NonFullScreenPageModeUseNone = 1 << 18;
    /** A viewer preference */
    public static final int NonFullScreenPageModeUseOutlines = 1 << 19;
    /** A viewer preference */
    public static final int NonFullScreenPageModeUseThumbs = 1 << 20;
    /** A viewer preference */
    public static final int NonFullScreenPageModeUseOC = 1 << 21;

    /** A viewer preference */
    public static final int DirectionL2R = 1 << 22;
    /** A viewer preference */
    public static final int DirectionR2L = 1 << 23;

    /** A viewer preference */
    public static final int PrintScalingNone = 1 << 24;

    /** @see com.itextpdf.text.pdf.interfaces.PdfViewerPreferences#setViewerPreferences(int) */
    public void setViewerPreferences(final int preferences) {
        pdf.setViewerPreferences(preferences);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfViewerPreferences#addViewerPreference(com.itextpdf.text.pdf.PdfName, com.itextpdf.text.pdf.PdfObject) */
    public void addViewerPreference(final PdfName key, final PdfObject value) {
        pdf.addViewerPreference(key, value);
    }

    //  [C4] Page labels

    /**
     * Use this method to add page labels
     * @param pageLabels the page labels
     */
    public void setPageLabels(final PdfPageLabels pageLabels) {
        pdf.setPageLabels(pageLabels);
    }

    //  [C5] named objects: named destinations, javascript, embedded files

    /**
     * Adds named destinations in bulk.
     * Valid keys and values of the map can be found in the map
     * that is created by SimpleNamedDestination.
     * @param   map   a map with strings as keys for the names,
     *          and structured strings as values for the destinations
     * @param   page_offset   number of pages that has to be added to
     *          the page numbers in the destinations (useful if you
     *          use this method in combination with PdfCopy).
     * @since   iText 5.0
     */
    public void addNamedDestinations(final Map<String, String> map, final int page_offset) {
        int page;
        String dest;
        PdfDestination destination;
        for (Map.Entry<String, String> entry : map.entrySet()) {
            dest = entry.getValue();
            page = Integer.parseInt(dest.substring(0, dest.indexOf(" ")));
            destination = new PdfDestination(dest.substring(dest.indexOf(" ") + 1));
            addNamedDestination(entry.getKey(), page + page_offset, destination);
        }
    }

    /**
     * Adds one named destination.
     * @param   name   the name for the destination
     * @param   page   the page number where you want to jump to
     * @param   dest   an explicit destination
     * @since   iText 5.0
     */
    public void addNamedDestination(final String name, final int page, final PdfDestination dest) {
        PdfDestination d = new PdfDestination(dest);
        d.addPage(getPageReference(page));
        pdf.localDestination(name, d);
    }

    /**
     * Use this method to add a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param js The JavaScript action
     */
    public void addJavaScript(final PdfAction js) {
        pdf.addJavaScript(js);
    }

    /**
     * Use this method to add a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param code the JavaScript code
     * @param unicode select JavaScript unicode. Note that the internal
     * Acrobat JavaScript engine does not support unicode,
     * so this may or may not work for you
     */
    public void addJavaScript(final String code, final boolean unicode) {
        addJavaScript(PdfAction.javaScript(code, this, unicode));
    }

    /**
     * Use this method to adds a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param code the JavaScript code
     */
    public void addJavaScript(final String code) {
        addJavaScript(code, false);
    }

    /**
     * Use this method to add a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param name   The name of the JS Action in the name tree
     * @param js The JavaScript action
     */
    public void addJavaScript(final String name, final PdfAction js) {
        pdf.addJavaScript(name, js);
    }

    /**
     * Use this method to add a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param name   The name of the JS Action in the name tree
     * @param code the JavaScript code
     * @param unicode select JavaScript unicode. Note that the internal
     * Acrobat JavaScript engine does not support unicode,
     * so this may or may not work for you
     */
    public void addJavaScript(final String name, final String code, final boolean unicode) {
        addJavaScript(name, PdfAction.javaScript(code, this, unicode));
    }

    /**
     * Use this method to adds a JavaScript action at the document level.
     * When the document opens, all this JavaScript runs.
     * @param name   The name of the JS Action in the name tree
     * @param code the JavaScript code
     */
    public void addJavaScript(final String name, final String code) {
        addJavaScript(name, code, false);
    }

    /**
     * Use this method to add a file attachment at the document level.
     * @param description the file description
     * @param fileStore an array with the file. If it's <CODE>null</CODE>
     * the file will be read from the disk
     * @param file the path to the file. It will only be used if
     * <CODE>fileStore</CODE> is not <CODE>null</CODE>
     * @param fileDisplay the actual file name stored in the pdf
     * @throws IOException on error
     */
    public void addFileAttachment(final String description, final byte fileStore[], final String file,
            final String fileDisplay) throws IOException {
        addFileAttachment(description, PdfFileSpecification.fileEmbedded(this, file, fileDisplay, fileStore));
    }

    /**
     * Use this method to add a file attachment at the document level.
     * @param description the file description
     * @param fs the file specification
    * @throws IOException if the file attachment could not be added to the document
     */
    public void addFileAttachment(final String description, final PdfFileSpecification fs) throws IOException {
        pdf.addFileAttachment(description, fs);
    }

    /**
     * Use this method to add a file attachment at the document level.
     * @param fs the file specification
    * @throws IOException if the file attachment could not be added to the document
     */
    public void addFileAttachment(final PdfFileSpecification fs) throws IOException {
        addFileAttachment(null, fs);
    }

    // [C6] Actions (open and additional)

    /** action value */
    public static final PdfName DOCUMENT_CLOSE = PdfName.WC;
    /** action value */
    public static final PdfName WILL_SAVE = PdfName.WS;
    /** action value */
    public static final PdfName DID_SAVE = PdfName.DS;
    /** action value */
    public static final PdfName WILL_PRINT = PdfName.WP;
    /** action value */
    public static final PdfName DID_PRINT = PdfName.DP;

    /** @see com.itextpdf.text.pdf.interfaces.PdfDocumentActions#setOpenAction(java.lang.String) */
    public void setOpenAction(final String name) {
        pdf.setOpenAction(name);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfDocumentActions#setOpenAction(com.itextpdf.text.pdf.PdfAction) */
    public void setOpenAction(final PdfAction action) {
        pdf.setOpenAction(action);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfDocumentActions#setAdditionalAction(com.itextpdf.text.pdf.PdfName, com.itextpdf.text.pdf.PdfAction) */
    public void setAdditionalAction(final PdfName actionType, final PdfAction action) throws DocumentException {
        if (!(actionType.equals(DOCUMENT_CLOSE) || actionType.equals(WILL_SAVE) || actionType.equals(DID_SAVE)
                || actionType.equals(WILL_PRINT) || actionType.equals(DID_PRINT))) {
            throw new DocumentException(MessageLocalization.getComposedMessage("invalid.additional.action.type.1",
                    actionType.toString()));
        }
        pdf.addAdditionalAction(actionType, action);
    }

    //  [C7] portable collections

    /**
     * Use this method to add the Collection dictionary.
     * @param collection a dictionary of type PdfCollection
     */
    public void setCollection(final PdfCollection collection) {
        setAtLeastPdfVersion(VERSION_1_7);
        pdf.setCollection(collection);
    }

    //  [C8] AcroForm

    /** signature value */
    public static final int SIGNATURE_EXISTS = 1;
    /** signature value */
    public static final int SIGNATURE_APPEND_ONLY = 2;

    /** @see com.itextpdf.text.pdf.interfaces.PdfAnnotations#getAcroForm() */
    public PdfAcroForm getAcroForm() {
        return pdf.getAcroForm();
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfAnnotations#addAnnotation(com.itextpdf.text.pdf.PdfAnnotation) */
    public void addAnnotation(final PdfAnnotation annot) {
        pdf.addAnnotation(annot);
    }

    void addAnnotation(final PdfAnnotation annot, final int page) {
        addAnnotation(annot);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfAnnotations#addCalculationOrder(com.itextpdf.text.pdf.PdfFormField) */
    public void addCalculationOrder(final PdfFormField annot) {
        pdf.addCalculationOrder(annot);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfAnnotations#setSigFlags(int) */
    public void setSigFlags(final int f) {
        pdf.setSigFlags(f);
    }

    public void setLanguage(final String language) {
        pdf.setLanguage(language);
    }

    //  [C9] Metadata

    /** XMP Metadata for the document. */
    protected byte[] xmpMetadata = null;

    /**
     * Use this method to set the XMP Metadata.
     * @param xmpMetadata The xmpMetadata to set.
     */
    public void setXmpMetadata(final byte[] xmpMetadata) {
        this.xmpMetadata = xmpMetadata;
    }

    /**
     * Use this method to set the XMP Metadata for each page.
     * @param xmpMetadata The xmpMetadata to set.
     * @throws IOException
     */
    public void setPageXmpMetadata(final byte[] xmpMetadata) throws IOException {
        pdf.setXmpMetadata(xmpMetadata);
    }

    protected XmpWriter xmpWriter = null;

    public XmpWriter getXmpWriter() {
        return xmpWriter;
    }

    /**
     * Use this method to creates XMP Metadata based
     * on the metadata in the PdfDocument.
     * @since 5.4.4 just creates XmpWriter instance which will be serialized in close.
     */
    public void createXmpMetadata() {
        try {
            xmpWriter = createXmpWriter(null, pdf.getInfo());
            if (isTagged()) {
                try {
                    xmpWriter.getXmpMeta().setPropertyInteger(XMPConst.NS_PDFUA_ID, PdfProperties.PART, 1,
                            new PropertyOptions(PropertyOptions.SEPARATE_NODE));
                } catch (XMPException e) {
                    throw new ExceptionConverter(e);
                }
            }
            xmpMetadata = null;
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    //  [C10] PDFX Conformance
    /** A PDF/X level. */
    public static final int PDFXNONE = 0;
    /** A PDF/X level. */
    public static final int PDFX1A2001 = 1;
    /** A PDF/X level. */
    public static final int PDFX32002 = 2;

    /** Stores the PDF ISO conformance. */
    protected PdfIsoConformance pdfIsoConformance = initPdfIsoConformance();

    protected PdfIsoConformance initPdfIsoConformance() {
        return new PdfXConformanceImp(this);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfXConformance#setPDFXConformance(int) */
    public void setPDFXConformance(final int pdfx) {
        if (!(pdfIsoConformance instanceof PdfXConformanceImp))
            return;
        if (((PdfXConformance) pdfIsoConformance).getPDFXConformance() == pdfx)
            return;
        if (pdf.isOpen())
            throw new PdfXConformanceException(MessageLocalization
                    .getComposedMessage("pdfx.conformance.can.only.be.set.before.opening.the.document"));
        if (crypto != null)
            throw new PdfXConformanceException(
                    MessageLocalization.getComposedMessage("a.pdfx.conforming.document.cannot.be.encrypted"));
        if (pdfx != PDFXNONE)
            setPdfVersion(VERSION_1_3);
        ((PdfXConformance) pdfIsoConformance).setPDFXConformance(pdfx);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfXConformance#getPDFXConformance() */
    public int getPDFXConformance() {
        if (pdfIsoConformance instanceof PdfXConformanceImp)
            return ((PdfXConformance) pdfIsoConformance).getPDFXConformance();
        else
            return PDFXNONE;
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfXConformance#isPdfX() */
    public boolean isPdfX() {
        if (pdfIsoConformance instanceof PdfXConformanceImp)
            return ((PdfXConformance) pdfIsoConformance).isPdfX();
        else
            return false;
    }

    /**
     * Checks if any PDF ISO conformance is necessary.
     * @return <code>true</code> if the PDF has to be in conformance with any of the PDF ISO specifications
     */
    public boolean isPdfIso() {
        return pdfIsoConformance.isPdfIso();
    }

    //  [C11] Output intents
    /**
     * Sets the values of the output intent dictionary. Null values are allowed to
     * suppress any key.
     *
     * @param outputConditionIdentifier a value
     * @param outputCondition           a value
     * @param registryName              a value
     * @param info                      a value
     * @param colorProfile              a value
     * @since 2.1.5
     * @throws IOException on error
     */
    public void setOutputIntents(final String outputConditionIdentifier, final String outputCondition,
            final String registryName, final String info, final ICC_Profile colorProfile) throws IOException {
        PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_OUTPUTINTENT, colorProfile);
        getExtraCatalog();
        PdfDictionary out = new PdfDictionary(PdfName.OUTPUTINTENT);
        if (outputCondition != null)
            out.put(PdfName.OUTPUTCONDITION, new PdfString(outputCondition, PdfObject.TEXT_UNICODE));
        if (outputConditionIdentifier != null)
            out.put(PdfName.OUTPUTCONDITIONIDENTIFIER,
                    new PdfString(outputConditionIdentifier, PdfObject.TEXT_UNICODE));
        if (registryName != null)
            out.put(PdfName.REGISTRYNAME, new PdfString(registryName, PdfObject.TEXT_UNICODE));
        if (info != null)
            out.put(PdfName.INFO, new PdfString(info, PdfObject.TEXT_UNICODE));
        if (colorProfile != null) {
            PdfStream stream = new PdfICCBased(colorProfile, compressionLevel);
            out.put(PdfName.DESTOUTPUTPROFILE, addToBody(stream).getIndirectReference());
        }

        out.put(PdfName.S, PdfName.GTS_PDFX);

        extraCatalog.put(PdfName.OUTPUTINTENTS, new PdfArray(out));
        this.colorProfile = colorProfile;
    }

    /**
      * Sets the values of the output intent dictionary. Null values are allowed to
      * suppress any key.
      *
      * Prefer the <CODE>ICC_Profile</CODE>-based version of this method.
      * @param outputConditionIdentifier a value
      * @param outputCondition           a value, "PDFA/A" to force GTS_PDFA1, otherwise cued by pdfxConformance.
      * @param registryName              a value
      * @param info                      a value
      * @param destOutputProfile         a value
      * @since 1.x
      *
      * @throws IOException
      */
    public void setOutputIntents(final String outputConditionIdentifier, final String outputCondition,
            final String registryName, final String info, final byte destOutputProfile[]) throws IOException {
        ICC_Profile colorProfile = destOutputProfile == null ? null : ICC_Profile.getInstance(destOutputProfile);
        setOutputIntents(outputConditionIdentifier, outputCondition, registryName, info, colorProfile);
    }

    /**
     * Use this method to copy the output intent dictionary
     * from another document to this one.
     * @param reader the other document
     * @param checkExistence <CODE>true</CODE> to just check for the existence of a valid output intent
     * dictionary, <CODE>false</CODE> to insert the dictionary if it exists
     * @throws IOException on error
     * @return <CODE>true</CODE> if the output intent dictionary exists, <CODE>false</CODE>
     * otherwise
     */
    public boolean setOutputIntents(final PdfReader reader, final boolean checkExistence) throws IOException {
        PdfDictionary catalog = reader.getCatalog();
        PdfArray outs = catalog.getAsArray(PdfName.OUTPUTINTENTS);
        if (outs == null)
            return false;
        if (outs.isEmpty())
            return false;
        PdfDictionary out = outs.getAsDict(0);
        PdfObject obj = PdfReader.getPdfObject(out.get(PdfName.S));
        if (obj == null || !PdfName.GTS_PDFX.equals(obj))
            return false;
        if (checkExistence)
            return true;
        PRStream stream = (PRStream) PdfReader.getPdfObject(out.get(PdfName.DESTOUTPUTPROFILE));
        byte destProfile[] = null;
        if (stream != null) {
            destProfile = PdfReader.getStreamBytes(stream);
        }
        setOutputIntents(getNameString(out, PdfName.OUTPUTCONDITIONIDENTIFIER),
                getNameString(out, PdfName.OUTPUTCONDITION), getNameString(out, PdfName.REGISTRYNAME),
                getNameString(out, PdfName.INFO), destProfile);
        return true;
    }

    protected static String getNameString(final PdfDictionary dic, final PdfName key) {
        PdfObject obj = PdfReader.getPdfObject(dic.get(key));
        if (obj == null || !obj.isString())
            return null;
        return ((PdfString) obj).toUnicodeString();
    }

    // PDF Objects that have an impact on the PDF body

    //  [F1] PdfEncryptionSettings interface

    // types of encryption

    /** Type of encryption */
    public static final int STANDARD_ENCRYPTION_40 = 0;
    /** Type of encryption */
    public static final int STANDARD_ENCRYPTION_128 = 1;
    /** Type of encryption */
    public static final int ENCRYPTION_AES_128 = 2;
    /** Type of encryption */
    public static final int ENCRYPTION_AES_256 = 3;
    /** Mask to separate the encryption type from the encryption mode. */
    static final int ENCRYPTION_MASK = 7;
    /** Add this to the mode to keep the metadata in clear text */
    public static final int DO_NOT_ENCRYPT_METADATA = 8;
    /**
     * Add this to the mode to keep encrypt only the embedded files.
     * @since 2.1.3
     */
    public static final int EMBEDDED_FILES_ONLY = 24;

    // permissions

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_PRINTING = 4 + 2048;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_MODIFY_CONTENTS = 8;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_COPY = 16;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_MODIFY_ANNOTATIONS = 32;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_FILL_IN = 256;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_SCREENREADERS = 512;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_ASSEMBLY = 1024;

    /** The operation permitted when the document is opened with the user password
     *
     * @since 2.0.7
     */
    public static final int ALLOW_DEGRADED_PRINTING = 4;

    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_PRINTING} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowPrinting = ALLOW_PRINTING;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_MODIFY_CONTENTS} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowModifyContents = ALLOW_MODIFY_CONTENTS;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_COPY} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowCopy = ALLOW_COPY;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_MODIFY_ANNOTATIONS} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowModifyAnnotations = ALLOW_MODIFY_ANNOTATIONS;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_FILL_IN} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowFillIn = ALLOW_FILL_IN;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_SCREENREADERS} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowScreenReaders = ALLOW_SCREENREADERS;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_ASSEMBLY} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowAssembly = ALLOW_ASSEMBLY;
    /** @deprecated As of iText 2.0.7, use {@link #ALLOW_DEGRADED_PRINTING} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final int AllowDegradedPrinting = ALLOW_DEGRADED_PRINTING;

    // Strength of the encryption (kept for historical reasons)
    /** @deprecated As of iText 2.0.7, use {@link #STANDARD_ENCRYPTION_40} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final boolean STRENGTH40BITS = false;
    /** @deprecated As of iText 2.0.7, use {@link #STANDARD_ENCRYPTION_128} instead. Scheduled for removal at or after 2.2.0 */
    @Deprecated
    public static final boolean STRENGTH128BITS = true;

    /** Contains the business logic for cryptography. */
    protected PdfEncryption crypto;

    PdfEncryption getEncryption() {
        return crypto;
    }

    /**
     *  Sets the encryption options for this document. The userPassword and the
     *  ownerPassword can be null or have zero length. In this case the ownerPassword
     *  is replaced by a random string. The open permissions for the document can be
     *  AllowPrinting, AllowModifyContents, AllowCopy, AllowModifyAnnotations,
     *  AllowFillIn, AllowScreenReaders, AllowAssembly and AllowDegradedPrinting.
     *  The permissions can be combined by ORing them.
     * @param userPassword the user password. Can be null or empty
     * @param ownerPassword the owner password. Can be null or empty
     * @param permissions the user permissions
     * @param encryptionType can be any of the following: PdfWriter#STANDARD_ENCRYPTION_40, PdfWriter#STANDARD_ENCRYPTION_128, PdfWriter#ENCRYPTION_AES_128 or PdfWriter#ENCRYPTION_AES_256
     * @throws DocumentException if the document is already open
     */
    public void setEncryption(final byte userPassword[], final byte ownerPassword[], final int permissions,
            final int encryptionType) throws DocumentException {
        if (pdf.isOpen())
            throw new DocumentException(MessageLocalization
                    .getComposedMessage("encryption.can.only.be.added.before.opening.the.document"));
        crypto = new PdfEncryption();
        crypto.setCryptoMode(encryptionType, 0);
        crypto.setupAllKeys(userPassword, ownerPassword, permissions);
    }

    /**
     *  Sets the encryption options for this document. The userPassword and the
     *  ownerPassword can be null or have zero length. In this case the ownerPassword
     *  is replaced by a random string. The open permissions for the document can be
     *  AllowPrinting, AllowModifyContents, AllowCopy, AllowModifyAnnotations,
     *  AllowFillIn, AllowScreenReaders, AllowAssembly and AllowDegradedPrinting.
     *  The permissions can be combined by ORing them.
     * @param certs Certificates to encrypt the document
     * @param permissions the user permissions
     * @param encryptionType can be any of the following: PdfWriter#STANDARD_ENCRYPTION_40, PdfWriter#STANDARD_ENCRYPTION_128, PdfWriter#ENCRYPTION_AES_128 or PdfWriter#ENCRYPTION_AES_256
     * @throws DocumentException if the document is already open
     */
    public void setEncryption(final Certificate[] certs, final int[] permissions, final int encryptionType)
            throws DocumentException {
        if (pdf.isOpen())
            throw new DocumentException(MessageLocalization
                    .getComposedMessage("encryption.can.only.be.added.before.opening.the.document"));
        crypto = new PdfEncryption();
        if (certs != null) {
            for (int i = 0; i < certs.length; i++) {
                crypto.addRecipient(certs[i], permissions[i]);
            }
        }
        crypto.setCryptoMode(encryptionType, 0);
        crypto.getEncryptionDictionary();
    }

    /**
     * Sets the encryption options for this document. The userPassword and the
     *  ownerPassword can be null or have zero length. In this case the ownerPassword
     *  is replaced by a random string. The open permissions for the document can be
     *  AllowPrinting, AllowModifyContents, AllowCopy, AllowModifyAnnotations,
     *  AllowFillIn, AllowScreenReaders, AllowAssembly and AllowDegradedPrinting.
     *  The permissions can be combined by ORing them.
     * @param userPassword the user password. Can be null or empty
     * @param ownerPassword the owner password. Can be null or empty
     * @param permissions the user permissions
     * @param strength128Bits <code>true</code> for 128 bit key length, <code>false</code> for 40 bit key length
     * @throws DocumentException if the document is already open
     * @deprecated As of iText 2.0.3, replaced by (@link #setEncryption(byte[], byte[], int, int)}. Scheduled for removal at or after 2.2.0
     */
    @Deprecated
    public void setEncryption(final byte userPassword[], final byte ownerPassword[], final int permissions,
            final boolean strength128Bits) throws DocumentException {
        setEncryption(userPassword, ownerPassword, permissions,
                strength128Bits ? STANDARD_ENCRYPTION_128 : STANDARD_ENCRYPTION_40);
    }

    /**
     * Sets the encryption options for this document. The userPassword and the
     *  ownerPassword can be null or have zero length. In this case the ownerPassword
     *  is replaced by a random string. The open permissions for the document can be
     *  AllowPrinting, AllowModifyContents, AllowCopy, AllowModifyAnnotations,
     *  AllowFillIn, AllowScreenReaders, AllowAssembly and AllowDegradedPrinting.
     *  The permissions can be combined by ORing them.
     * @param strength <code>true</code> for 128 bit key length, <code>false</code> for 40 bit key length
     * @param userPassword the user password. Can be null or empty
     * @param ownerPassword the owner password. Can be null or empty
     * @param permissions the user permissions
     * @throws DocumentException if the document is already open
     * @deprecated As of iText 2.0.3, replaced by (@link #setEncryption(byte[], byte[], int, int)}. Scheduled for removal at or after 2.2.0
     */
    @Deprecated
    public void setEncryption(final boolean strength, final String userPassword, final String ownerPassword,
            final int permissions) throws DocumentException {
        setEncryption(getISOBytes(userPassword), getISOBytes(ownerPassword), permissions,
                strength ? STANDARD_ENCRYPTION_128 : STANDARD_ENCRYPTION_40);
    }

    /**
     * Sets the encryption options for this document. The userPassword and the
     *  ownerPassword can be null or have zero length. In this case the ownerPassword
     *  is replaced by a random string. The open permissions for the document can be
     *  AllowPrinting, AllowModifyContents, AllowCopy, AllowModifyAnnotations,
     *  AllowFillIn, AllowScreenReaders, AllowAssembly and AllowDegradedPrinting.
     *  The permissions can be combined by ORing them.
     * @param encryptionType the type of encryption. It can be one of STANDARD_ENCRYPTION_40, STANDARD_ENCRYPTION_128 or ENCRYPTION_AES128.
     * Optionally DO_NOT_ENCRYPT_METADATA can be ored to output the metadata in cleartext
     * @param userPassword the user password. Can be null or empty
     * @param ownerPassword the owner password. Can be null or empty
     * @param permissions the user permissions
     * @throws DocumentException if the document is already open
     * @deprecated As of iText 2.0.3, replaced by (@link #setEncryption(byte[], byte[], int, int)}. Scheduled for removal at or after 2.2.0
     */
    @Deprecated
    public void setEncryption(final int encryptionType, final String userPassword, final String ownerPassword,
            final int permissions) throws DocumentException {
        setEncryption(getISOBytes(userPassword), getISOBytes(ownerPassword), permissions, encryptionType);
    }

    //  [F2] compression

    /** Holds value of property fullCompression. */
    protected boolean fullCompression = false;

    /**
     * Use this method to find out if 1.5 compression is on.
     * @return the 1.5 compression status
     */
    public boolean isFullCompression() {
        return this.fullCompression;
    }

    /**
     * Use this method to set the document's compression to the
     * PDF 1.5 mode with object streams and xref streams.
     * It can be set at any time but once set it can't be unset.
     */
    public void setFullCompression() throws DocumentException {
        if (open)
            throw new DocumentException(MessageLocalization
                    .getComposedMessage("you.can.t.set.the.full.compression.if.the.document.is.already.open"));
        this.fullCompression = true;
        setAtLeastPdfVersion(VERSION_1_5);
    }

    /**
     * The compression level of the content streams.
     * @since 2.1.3
     */
    protected int compressionLevel = PdfStream.DEFAULT_COMPRESSION;

    /**
     * Returns the compression level used for streams written by this writer.
     * @return the compression level (0 = best speed, 9 = best compression, -1 is default)
     * @since 2.1.3
     */
    public int getCompressionLevel() {
        return compressionLevel;
    }

    /**
     * Sets the compression level to be used for streams written by this writer.
     * @param compressionLevel a value between 0 (best speed) and 9 (best compression)
     * @since 2.1.3
     */
    public void setCompressionLevel(final int compressionLevel) {
        if (compressionLevel < PdfStream.NO_COMPRESSION || compressionLevel > PdfStream.BEST_COMPRESSION)
            this.compressionLevel = PdfStream.DEFAULT_COMPRESSION;
        else
            this.compressionLevel = compressionLevel;
    }

    //  [F3] adding fonts

    /** The fonts of this document */
    protected LinkedHashMap<BaseFont, FontDetails> documentFonts = new LinkedHashMap<BaseFont, FontDetails>();

    /** The font number counter for the fonts in the document. */
    protected int fontNumber = 1;

    /**
     * Adds a <CODE>BaseFont</CODE> to the document but not to the page resources.
     * It is used for templates.
     * @param bf the <CODE>BaseFont</CODE> to add
     * @return an <CODE>Object[]</CODE> where position 0 is a <CODE>PdfName</CODE>
     * and position 1 is an <CODE>PdfIndirectReference</CODE>
     */

    FontDetails addSimple(final BaseFont bf) {
        FontDetails ret = documentFonts.get(bf);
        if (ret == null) {
            PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_FONT, bf);
            if (bf.getFontType() == BaseFont.FONT_TYPE_DOCUMENT) {
                ret = new FontDetails(new PdfName("F" + fontNumber++), ((DocumentFont) bf).getIndirectReference(),
                        bf);
            } else {
                ret = new FontDetails(new PdfName("F" + fontNumber++), body.getPdfIndirectReference(), bf);
            }
            documentFonts.put(bf, ret);
        }
        return ret;
    }

    void eliminateFontSubset(final PdfDictionary fonts) {
        for (Object element : documentFonts.values()) {
            FontDetails ft = (FontDetails) element;
            if (fonts.get(ft.getFontName()) != null)
                ft.setSubset(false);
        }
    }

    //  [F4] adding (and releasing) form XObjects

    /** The form XObjects in this document. The key is the xref and the value
    is Object[]{PdfName, template}.*/
    protected HashMap<PdfIndirectReference, Object[]> formXObjects = new HashMap<PdfIndirectReference, Object[]>();

    /** The name counter for the form XObjects name. */
    protected int formXObjectsCounter = 1;

    /**
     * Adds a template to the document but not to the page resources.
     * @param template the template to add
     * @param forcedName the template name, rather than a generated one. Can be null
     * @return the <CODE>PdfName</CODE> for this template
     */

    PdfName addDirectTemplateSimple(PdfTemplate template, final PdfName forcedName) {
        PdfIndirectReference ref = template.getIndirectReference();
        Object obj[] = formXObjects.get(ref);
        PdfName name = null;
        try {
            if (obj == null) {
                if (forcedName == null) {
                    name = new PdfName("Xf" + formXObjectsCounter);
                    ++formXObjectsCounter;
                } else
                    name = forcedName;
                if (template.getType() == PdfTemplate.TYPE_IMPORTED) {
                    // If we got here from PdfCopy we'll have to fill importedPages
                    PdfImportedPage ip = (PdfImportedPage) template;
                    PdfReader r = ip.getPdfReaderInstance().getReader();
                    if (!readerInstances.containsKey(r)) {
                        readerInstances.put(r, ip.getPdfReaderInstance());
                    }
                    template = null;
                }
                formXObjects.put(ref, new Object[] { name, template });
            } else
                name = (PdfName) obj[0];
        } catch (Exception e) {
            throw new ExceptionConverter(e);
        }
        return name;
    }

    /**
     * Use this method to releases the memory used by a template.
     * This method writes the template to the output.
     * The template can still be added to any content
     * but changes to the template itself won't have any effect.
     * @param tp the template to release
     * @throws IOException on error
     */
    public void releaseTemplate(final PdfTemplate tp) throws IOException {
        PdfIndirectReference ref = tp.getIndirectReference();
        Object[] objs = formXObjects.get(ref);
        if (objs == null || objs[1] == null)
            return;
        PdfTemplate template = (PdfTemplate) objs[1];
        if (template.getIndirectReference() instanceof PRIndirectReference)
            return;
        if (template.getType() == PdfTemplate.TYPE_TEMPLATE) {
            addToBody(template.getFormXObject(compressionLevel), template.getIndirectReference());
            objs[1] = null;
        }
    }

    //  [F5] adding pages imported form other PDF documents

    /**
     * Instances of PdfReader/PdfReaderInstance that are used to import pages.
     * @since 5.0.3
     */
    protected HashMap<PdfReader, PdfReaderInstance> readerInstances = new HashMap<PdfReader, PdfReaderInstance>();

    /**
     * Use this method to get a page from other PDF document.
     * The page can be used as any other PdfTemplate.
     * Note that calling this method more than once with the same parameters
     * will retrieve the same object.
     * @param reader the PDF document where the page is
     * @param pageNumber the page number. The first page is 1
     * @return the template representing the imported page
     */
    public PdfImportedPage getImportedPage(final PdfReader reader, final int pageNumber) {
        return getPdfReaderInstance(reader).getImportedPage(pageNumber);
    }

    /**
     * Returns the PdfReaderInstance associated with the specified reader.
     * Multiple calls with the same reader object will return the same
     * PdfReaderInstance.
     * @param reader the PDF reader that you want an instance for
     * @return the instance for the provided reader
     * @since 5.0.3
     */
    protected PdfReaderInstance getPdfReaderInstance(final PdfReader reader) {
        PdfReaderInstance inst = readerInstances.get(reader);
        if (inst == null) {
            inst = reader.getPdfReaderInstance(this);
            readerInstances.put(reader, inst);
        }
        return inst;
    }

    /**
     * Use this method to writes the reader to the document
     * and free the memory used by it.
     * The main use is when concatenating multiple documents
     * to keep the memory usage restricted to the current
     * appending document.
     * @param reader the <CODE>PdfReader</CODE> to free
     * @throws IOException on error
     */
    public void freeReader(final PdfReader reader) throws IOException {
        currentPdfReaderInstance = readerInstances.get(reader);
        if (currentPdfReaderInstance == null)
            return;
        currentPdfReaderInstance.writeAllPages();
        currentPdfReaderInstance = null;
        readerInstances.remove(reader);
    }

    /**
     * Use this method to gets the current document size.
     * This size only includes the data already written
     * to the output stream, it does not include templates or fonts.
     * It is useful if used with <CODE>freeReader()</CODE>
     * when concatenating many documents and an idea of
     * the current size is needed.
     * @return the approximate size without fonts or templates
     */
    public long getCurrentDocumentSize() {
        return body.offset() + body.size() * 20 + 0x48;
    }

    protected PdfReaderInstance currentPdfReaderInstance;

    protected int getNewObjectNumber(final PdfReader reader, final int number, final int generation) {
        if (currentPdfReaderInstance == null || currentPdfReaderInstance.getReader() != reader) {
            currentPdfReaderInstance = getPdfReaderInstance(reader);
        }
        return currentPdfReaderInstance.getNewObjectNumber(number, generation);
    }

    RandomAccessFileOrArray getReaderFile(final PdfReader reader) {
        return currentPdfReaderInstance.getReaderFile();
    }

    //  [F6] spot colors

    /** The colors of this document */
    protected HashMap<ICachedColorSpace, ColorDetails> documentColors = new HashMap<ICachedColorSpace, ColorDetails>();

    /** The color number counter for the colors in the document. */
    protected int colorNumber = 1;

    PdfName getColorspaceName() {
        return new PdfName("CS" + colorNumber++);
    }

    /**
     * Adds a <CODE>SpotColor</CODE> to the document but not to the page resources.
     * @param spc the <CODE>SpotColor</CODE> to add
     * @return an <CODE>Object[]</CODE> where position 0 is a <CODE>PdfName</CODE>
     * and position 1 is an <CODE>PdfIndirectReference</CODE>
     */
    ColorDetails addSimple(final ICachedColorSpace spc) {
        ColorDetails ret = documentColors.get(spc);
        if (ret == null) {
            ret = new ColorDetails(getColorspaceName(), body.getPdfIndirectReference(), spc);
            if (spc instanceof IPdfSpecialColorSpace) {
                ((IPdfSpecialColorSpace) spc).getColorantDetails(this);
            }
            documentColors.put(spc, ret);
        }
        return ret;
    }

    //  [F7] document patterns

    /** The patterns of this document */
    protected HashMap<PdfPatternPainter, PdfName> documentPatterns = new HashMap<PdfPatternPainter, PdfName>();

    /** The pattern number counter for the colors in the document. */
    protected int patternNumber = 1;

    PdfName addSimplePattern(final PdfPatternPainter painter) {
        PdfName name = documentPatterns.get(painter);
        try {
            if (name == null) {
                name = new PdfName("P" + patternNumber);
                ++patternNumber;
                documentPatterns.put(painter, name);
            }
        } catch (Exception e) {
            throw new ExceptionConverter(e);
        }
        return name;
    }

    //  [F8] shading patterns

    protected HashSet<PdfShadingPattern> documentShadingPatterns = new HashSet<PdfShadingPattern>();

    void addSimpleShadingPattern(final PdfShadingPattern shading) {
        if (!documentShadingPatterns.contains(shading)) {
            shading.setName(patternNumber);
            ++patternNumber;
            documentShadingPatterns.add(shading);
            addSimpleShading(shading.getShading());
        }
    }

    //  [F9] document shadings

    protected HashSet<PdfShading> documentShadings = new HashSet<PdfShading>();

    void addSimpleShading(final PdfShading shading) {
        if (!documentShadings.contains(shading)) {
            documentShadings.add(shading);
            shading.setName(documentShadings.size());
        }
    }

    // [F10] extended graphics state (for instance for transparency)

    protected HashMap<PdfDictionary, PdfObject[]> documentExtGState = new HashMap<PdfDictionary, PdfObject[]>();

    PdfObject[] addSimpleExtGState(final PdfDictionary gstate) {
        if (!documentExtGState.containsKey(gstate)) {
            documentExtGState.put(gstate, new PdfObject[] { new PdfName("GS" + (documentExtGState.size() + 1)),
                    getPdfIndirectReference() });
        }
        return documentExtGState.get(gstate);
    }

    //  [F11] adding properties (OCG, marked content)

    protected HashMap<Object, PdfObject[]> documentProperties = new HashMap<Object, PdfObject[]>();

    PdfObject[] addSimpleProperty(final Object prop, final PdfIndirectReference refi) {
        if (!documentProperties.containsKey(prop)) {
            if (prop instanceof PdfOCG)
                PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_LAYER, prop);
            documentProperties.put(prop,
                    new PdfObject[] { new PdfName("Pr" + (documentProperties.size() + 1)), refi });
        }
        return documentProperties.get(prop);
    }

    boolean propertyExists(final Object prop) {
        return documentProperties.containsKey(prop);
    }

    //  [F12] tagged PDF

    static public final int markAll = 0x00;
    static public final int markInlineElementsOnly = 0x01;

    protected boolean tagged = false;
    protected int taggingMode = markInlineElementsOnly;
    protected PdfStructureTreeRoot structureTreeRoot;

    /**
     * Mark this document for tagging. It must be called before open.
     */
    public void setTagged() {
        setTagged(markInlineElementsOnly);
    }

    public void setTagged(int taggingMode) {
        if (open)
            throw new IllegalArgumentException(
                    MessageLocalization.getComposedMessage("tagging.must.be.set.before.opening.the.document"));
        tagged = true;
        this.taggingMode = taggingMode;
    }

    public boolean needToBeMarkedInContent(IAccessibleElement element) {
        if ((taggingMode & markInlineElementsOnly) != 0) {
            if (element.isInline() || PdfName.ARTIFACT.equals(element.getRole())) {
                return true;
            }
            return false;
        }
        return true;
    }

    public void checkElementRole(IAccessibleElement element, IAccessibleElement parent) {
        if (parent != null && (parent.getRole() == null || PdfName.ARTIFACT.equals(parent.getRole())))
            element.setRole(null);
        else if ((taggingMode & markInlineElementsOnly) != 0) {
            if (element.isInline() && element.getRole() == null && (parent == null || !parent.isInline()))
                throw new IllegalArgumentException(
                        MessageLocalization.getComposedMessage("inline.elements.with.role.null.are.not.allowed"));
        }
    }

    /**
     * Check if the document is marked for tagging.
     * @return <CODE>true</CODE> if the document is marked for tagging
     */
    public boolean isTagged() {
        return tagged;
    }

    /**
     * Fix structure of tagged document: remove unused objects, remove unused items from class map,
     * fix xref table due to removed objects.
     */
    protected void flushTaggedObjects() throws IOException {
    }

    protected void flushAcroFields() throws IOException, BadPdfFormatException {
    }

    /**
     * Gets the structure tree root. If the document is not marked for tagging it will return <CODE>null</CODE>.
     * @return the structure tree root
     */
    public PdfStructureTreeRoot getStructureTreeRoot() {
        if (tagged && structureTreeRoot == null)
            structureTreeRoot = new PdfStructureTreeRoot(this);
        return structureTreeRoot;
    }

    //  [F13] Optional Content Groups
    /** A hashSet containing all the PdfLayer objects. */
    protected LinkedHashSet<PdfOCG> documentOCG = new LinkedHashSet<PdfOCG>();
    /** An array list used to define the order of an OCG tree. */
    protected ArrayList<PdfOCG> documentOCGorder = new ArrayList<PdfOCG>();
    /** The OCProperties in a catalog dictionary. */
    protected PdfOCProperties OCProperties;
    /** The RBGroups array in an OCG dictionary */
    protected PdfArray OCGRadioGroup = new PdfArray();
    /**
     * The locked array in an OCG dictionary
     * @since   2.1.2
     */
    protected PdfArray OCGLocked = new PdfArray();

    /**
     * Use this method to get the <B>Optional Content Properties Dictionary</B>.
     * Each call fills the dictionary with the current layer state.
     * It's advisable to only call this method right before close
     * and do any modifications at that time.
     * @return the Optional Content Properties Dictionary
     */
    public PdfOCProperties getOCProperties() {
        fillOCProperties(true);
        return OCProperties;
    }

    /**
     * Use this method to set a collection of optional content groups
     * whose states are intended to follow a "radio button" paradigm.
     * That is, the state of at most one optional content group
     * in the array should be ON at a time: if one group is turned
     * ON, all others must be turned OFF.
     * @param group the radio group
     */
    public void addOCGRadioGroup(final ArrayList<PdfLayer> group) {
        PdfArray ar = new PdfArray();
        for (int k = 0; k < group.size(); ++k) {
            PdfLayer layer = group.get(k);
            if (layer.getTitle() == null)
                ar.add(layer.getRef());
        }
        if (ar.size() == 0)
            return;
        OCGRadioGroup.add(ar);
    }

    /**
     * Use this method to lock an optional content group.
     * The state of a locked group cannot be changed through the user interface
     * of a viewer application. Producers can use this entry to prevent the visibility
     * of content that depends on these groups from being changed by users.
     * @param layer   the layer that needs to be added to the array of locked OCGs
     * @since   2.1.2
     */
    public void lockLayer(final PdfLayer layer) {
        OCGLocked.add(layer.getRef());
    }

    private static void getOCGOrder(final PdfArray order, final PdfLayer layer) {
        if (!layer.isOnPanel())
            return;
        if (layer.getTitle() == null)
            order.add(layer.getRef());
        ArrayList<PdfLayer> children = layer.getChildren();
        if (children == null)
            return;
        PdfArray kids = new PdfArray();
        if (layer.getTitle() != null)
            kids.add(new PdfString(layer.getTitle(), PdfObject.TEXT_UNICODE));
        for (int k = 0; k < children.size(); ++k) {
            getOCGOrder(kids, children.get(k));
        }
        if (kids.size() > 0)
            order.add(kids);
    }

    private void addASEvent(final PdfName event, final PdfName category) {
        PdfArray arr = new PdfArray();
        for (Object element : documentOCG) {
            PdfLayer layer = (PdfLayer) element;
            PdfDictionary usage = layer.getAsDict(PdfName.USAGE);
            if (usage != null && usage.get(category) != null)
                arr.add(layer.getRef());
        }
        if (arr.size() == 0)
            return;
        PdfDictionary d = OCProperties.getAsDict(PdfName.D);
        PdfArray arras = d.getAsArray(PdfName.AS);
        if (arras == null) {
            arras = new PdfArray();
            d.put(PdfName.AS, arras);
        }
        PdfDictionary as = new PdfDictionary();
        as.put(PdfName.EVENT, event);
        as.put(PdfName.CATEGORY, new PdfArray(category));
        as.put(PdfName.OCGS, arr);
        arras.add(as);
    }

    /**
     * @param erase true to erase the {@link PdfName#OCGS} and {@link PdfName#D} from the OCProperties first.
     * @since 2.1.2
     */
    protected void fillOCProperties(final boolean erase) {
        if (OCProperties == null)
            OCProperties = new PdfOCProperties();
        if (erase) {
            OCProperties.remove(PdfName.OCGS);
            OCProperties.remove(PdfName.D);
        }
        if (OCProperties.get(PdfName.OCGS) == null) {
            PdfArray gr = new PdfArray();
            for (Object element : documentOCG) {
                PdfLayer layer = (PdfLayer) element;
                gr.add(layer.getRef());
            }
            OCProperties.put(PdfName.OCGS, gr);
        }
        if (OCProperties.get(PdfName.D) != null)
            return;
        ArrayList<PdfOCG> docOrder = new ArrayList<PdfOCG>(documentOCGorder);
        for (Iterator<PdfOCG> it = docOrder.iterator(); it.hasNext();) {
            PdfLayer layer = (PdfLayer) it.next();
            if (layer.getParent() != null)
                it.remove();
        }
        PdfArray order = new PdfArray();
        for (Object element : docOrder) {
            PdfLayer layer = (PdfLayer) element;
            getOCGOrder(order, layer);
        }
        PdfDictionary d = new PdfDictionary();
        OCProperties.put(PdfName.D, d);
        d.put(PdfName.ORDER, order);
        if (docOrder.size() > 0 && (docOrder.get(0) instanceof PdfLayer)) {
            PdfLayer l = (PdfLayer) docOrder.get(0);
            PdfString name = l.getAsString(PdfName.NAME);
            if (name != null) {
                d.put(PdfName.NAME, name);
            }
        }
        PdfArray gr = new PdfArray();
        for (Object element : documentOCG) {
            PdfLayer layer = (PdfLayer) element;
            if (!layer.isOn())
                gr.add(layer.getRef());
        }
        if (gr.size() > 0)
            d.put(PdfName.OFF, gr);
        if (OCGRadioGroup.size() > 0)
            d.put(PdfName.RBGROUPS, OCGRadioGroup);
        if (OCGLocked.size() > 0)
            d.put(PdfName.LOCKED, OCGLocked);
        addASEvent(PdfName.VIEW, PdfName.ZOOM);
        addASEvent(PdfName.VIEW, PdfName.VIEW);
        addASEvent(PdfName.PRINT, PdfName.PRINT);
        addASEvent(PdfName.EXPORT, PdfName.EXPORT);
        d.put(PdfName.LISTMODE, PdfName.VISIBLEPAGES);
    }

    void registerLayer(final PdfOCG layer) {
        PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_LAYER, layer);
        if (layer instanceof PdfLayer) {
            PdfLayer la = (PdfLayer) layer;
            if (la.getTitle() == null) {
                if (!documentOCG.contains(layer)) {
                    documentOCG.add(layer);
                    documentOCGorder.add(layer);
                }
            } else {
                documentOCGorder.add(layer);
            }
        } else
            throw new IllegalArgumentException(MessageLocalization.getComposedMessage("only.pdflayer.is.accepted"));
    }

    //  User methods to change aspects of the page

    //  [U1] page size

    /**
     * Use this method to get the size of the media box.
     * @return a Rectangle
     */
    public Rectangle getPageSize() {
        return pdf.getPageSize();
    }

    /**
     * Use this method to set the crop box.
     * The crop box should not be rotated even if the page is rotated.
     * This change only takes effect in the next page.
     * @param crop the crop box
     */
    public void setCropBoxSize(final Rectangle crop) {
        pdf.setCropBoxSize(crop);
    }

    /**
     * Use this method to set the page box sizes.
     * Allowed names are: "crop", "trim", "art" and "bleed".
     * @param boxName the box size
     * @param size the size
     */
    public void setBoxSize(final String boxName, final Rectangle size) {
        pdf.setBoxSize(boxName, size);
    }

    /**
     * Use this method to get the size of a trim, art, crop or bleed box,
     * or null if not defined.
     * @param boxName crop, trim, art or bleed
     */
    public Rectangle getBoxSize(final String boxName) {
        return pdf.getBoxSize(boxName);
    }

    /**
     * Returns the intersection between the crop, trim art or bleed box and the parameter intersectingRectangle.
     * This method returns null when
     * - there is no intersection
     * - any of the above boxes are not defined
     * - the parameter intersectingRectangle is null
     *
     * @param boxName crop, trim, art, bleed
     * @param intersectingRectangle the rectangle that intersects the rectangle associated to the boxName
     * @return the intersection of the two rectangles
     */
    public Rectangle getBoxSize(final String boxName, final Rectangle intersectingRectangle) {
        Rectangle pdfRectangle = pdf.getBoxSize(boxName);

        if (pdfRectangle == null || intersectingRectangle == null) { // no intersection
            return null;
        }

        com.itextpdf.awt.geom.Rectangle boxRect = new com.itextpdf.awt.geom.Rectangle(pdfRectangle);
        com.itextpdf.awt.geom.Rectangle intRect = new com.itextpdf.awt.geom.Rectangle(intersectingRectangle);
        com.itextpdf.awt.geom.Rectangle outRect = boxRect.intersection(intRect);

        if (outRect.isEmpty()) { // no intersection
            return null;
        }

        Rectangle output = new Rectangle((float) outRect.getX(), (float) outRect.getY(),
                (float) (outRect.getX() + outRect.getWidth()), (float) (outRect.getY() + outRect.getHeight()));
        output.normalize();
        return output;
    }

    //  [U2] take care of empty pages

    /**
     * Use this method to make sure a page is added,
     * even if it's empty. If you use setPageEmpty(false),
     * invoking newPage() after a blank page will add a newPage.
     * setPageEmpty(true) won't have any effect.
     * @param pageEmpty the state
     */
    public void setPageEmpty(final boolean pageEmpty) {
        if (pageEmpty)
            return;
        pdf.setPageEmpty(pageEmpty);
    }

    /**
     * Checks if a newPage() will actually generate a new page.
     * @return true if a new page will be generated, false otherwise
     * @since 5.0.0
     */
    public boolean isPageEmpty() {
        return pdf.isPageEmpty();
    }

    //  [U3] page actions (open and close)

    /** action value */
    public static final PdfName PAGE_OPEN = PdfName.O;
    /** action value */
    public static final PdfName PAGE_CLOSE = PdfName.C;

    /** @see com.itextpdf.text.pdf.interfaces.PdfPageActions#setPageAction(com.itextpdf.text.pdf.PdfName, com.itextpdf.text.pdf.PdfAction) */
    public void setPageAction(final PdfName actionType, final PdfAction action) throws DocumentException {
        if (!actionType.equals(PAGE_OPEN) && !actionType.equals(PAGE_CLOSE))
            throw new DocumentException(MessageLocalization
                    .getComposedMessage("invalid.page.additional.action.type.1", actionType.toString()));
        pdf.setPageAction(actionType, action);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfPageActions#setDuration(int) */
    public void setDuration(final int seconds) {
        pdf.setDuration(seconds);
    }

    /** @see com.itextpdf.text.pdf.interfaces.PdfPageActions#setTransition(com.itextpdf.text.pdf.PdfTransition) */
    public void setTransition(final PdfTransition transition) {
        pdf.setTransition(transition);
    }

    //  [U4] Thumbnail image

    /**
     * Use this method to set the thumbnail image for the current page.
     * @param image the image
     * @throws PdfException on error
     * @throws DocumentException or error
     */
    public void setThumbnail(final Image image) throws PdfException, DocumentException {
        pdf.setThumbnail(image);
    }

    //  [U5] Transparency groups

    /**
     * A group attributes dictionary specifying the attributes
     * of the page's page group for use in the transparent
     * imaging model
     */
    protected PdfDictionary group;

    /**
     * Use this method to get the group dictionary.
     * @return Value of property group.
     */
    public PdfDictionary getGroup() {
        return this.group;
    }

    /**
     * Use this method to set the group dictionary.
     * @param group New value of property group.
     */
    public void setGroup(final PdfDictionary group) {
        this.group = group;
    }

    //  [U6] space char ratio

    /** The default space-char ratio. */
    public static final float SPACE_CHAR_RATIO_DEFAULT = 2.5f;
    /** Disable the inter-character spacing. */
    public static final float NO_SPACE_CHAR_RATIO = 10000000f;

    /**
     * The ratio between the extra word spacing and the extra character spacing.
     * Extra word spacing will grow <CODE>ratio</CODE> times more than extra character spacing.
     */
    private float spaceCharRatio = SPACE_CHAR_RATIO_DEFAULT;

    /**
     * Use this method to gets the space/character extra spacing ratio
     * for fully justified text.
     * @return the space/character extra spacing ratio
     */
    public float getSpaceCharRatio() {
        return spaceCharRatio;
    }

    /**
     * Use this method to set the ratio between the extra word spacing and
     * the extra character spacing when the text is fully justified.
     * Extra word spacing will grow <CODE>spaceCharRatio</CODE> times more
     * than extra character spacing. If the ratio is <CODE>PdfWriter.NO_SPACE_CHAR_RATIO</CODE>
     * then the extra character spacing will be zero.
     * @param spaceCharRatio the ratio between the extra word spacing and the extra character spacing
     */
    public void setSpaceCharRatio(final float spaceCharRatio) {
        if (spaceCharRatio < 0.001f)
            this.spaceCharRatio = 0.001f;
        else
            this.spaceCharRatio = spaceCharRatio;
    }

    //  [U7] run direction (doesn't actually do anything)

    /** Use the default run direction. */
    public static final int RUN_DIRECTION_DEFAULT = 0;
    /** Do not use bidirectional reordering. */
    public static final int RUN_DIRECTION_NO_BIDI = 1;
    /** Use bidirectional reordering with left-to-right
     * preferential run direction.
     */
    public static final int RUN_DIRECTION_LTR = 2;
    /** Use bidirectional reordering with right-to-left
     * preferential run direction.
     */
    public static final int RUN_DIRECTION_RTL = 3;

    protected int runDirection = RUN_DIRECTION_NO_BIDI;

    /**
     * Use this method to set the run direction.
     * This is only used as a placeholder as it does not affect anything.
     * @param runDirection the run direction
     */
    public void setRunDirection(final int runDirection) {
        if (runDirection < RUN_DIRECTION_NO_BIDI || runDirection > RUN_DIRECTION_RTL)
            throw new RuntimeException(
                    MessageLocalization.getComposedMessage("invalid.run.direction.1", runDirection));
        this.runDirection = runDirection;
    }

    /**
     * Use this method to set the run direction.
     * @return the run direction
     */
    public int getRunDirection() {
        return runDirection;
    }

    //  [U8] user units
    /**
     * Use this method to set the user unit.
     * A UserUnit is a value that defines the default user space unit.
     * The minimum UserUnit is 1 (1 unit = 1/72 inch).
     * The maximum UserUnit is 75,000.
     * Note that this userunit only works starting with PDF1.6!
     * @param userunit The userunit to set.
     * @throws DocumentException on error
     */
    public void setUserunit(final float userunit) throws DocumentException {
        if (userunit < 1f || userunit > 75000f)
            throw new DocumentException(
                    MessageLocalization.getComposedMessage("userunit.should.be.a.value.between.1.and.75000"));
        addPageDictEntry(PdfName.USERUNIT, new PdfNumber(userunit));
        setAtLeastPdfVersion(VERSION_1_6);
    }

    // Miscellaneous topics

    //  [M1] Color settings

    protected PdfDictionary defaultColorspace = new PdfDictionary();

    /**
     * Use this method to get the default colorspaces.
     * @return the default colorspaces
     */
    public PdfDictionary getDefaultColorspace() {
        return defaultColorspace;
    }

    /**
     * Use this method to sets the default colorspace that will be applied
     * to all the document. The colorspace is only applied if another colorspace
     * with the same name is not present in the content.
     * <p>
     * The colorspace is applied immediately when creating templates and
     * at the page end for the main document content.
     * @param key the name of the colorspace. It can be <CODE>PdfName.DEFAULTGRAY</CODE>, <CODE>PdfName.DEFAULTRGB</CODE>
     * or <CODE>PdfName.DEFAULTCMYK</CODE>
     * @param cs the colorspace. A <CODE>null</CODE> or <CODE>PdfNull</CODE> removes any colorspace with the same name
     */
    public void setDefaultColorspace(final PdfName key, final PdfObject cs) {
        if (cs == null || cs.isNull())
            defaultColorspace.remove(key);
        defaultColorspace.put(key, cs);
    }

    //  [M2] spot patterns

    protected HashMap<ColorDetails, ColorDetails> documentSpotPatterns = new HashMap<ColorDetails, ColorDetails>();
    protected ColorDetails patternColorspaceRGB;
    protected ColorDetails patternColorspaceGRAY;
    protected ColorDetails patternColorspaceCMYK;

    ColorDetails addSimplePatternColorspace(final BaseColor color) {
        int type = ExtendedColor.getType(color);
        if (type == ExtendedColor.TYPE_PATTERN || type == ExtendedColor.TYPE_SHADING)
            throw new RuntimeException(MessageLocalization.getComposedMessage(
                    "an.uncolored.tile.pattern.can.not.have.another.pattern.or.shading.as.color"));
        try {
            switch (type) {
            case ExtendedColor.TYPE_RGB:
                if (patternColorspaceRGB == null) {
                    patternColorspaceRGB = new ColorDetails(getColorspaceName(), body.getPdfIndirectReference(),
                            null);
                    PdfArray array = new PdfArray(PdfName.PATTERN);
                    array.add(PdfName.DEVICERGB);
                    addToBody(array, patternColorspaceRGB.getIndirectReference());
                }
                return patternColorspaceRGB;
            case ExtendedColor.TYPE_CMYK:
                if (patternColorspaceCMYK == null) {
                    patternColorspaceCMYK = new ColorDetails(getColorspaceName(), body.getPdfIndirectReference(),
                            null);
                    PdfArray array = new PdfArray(PdfName.PATTERN);
                    array.add(PdfName.DEVICECMYK);
                    addToBody(array, patternColorspaceCMYK.getIndirectReference());
                }
                return patternColorspaceCMYK;
            case ExtendedColor.TYPE_GRAY:
                if (patternColorspaceGRAY == null) {
                    patternColorspaceGRAY = new ColorDetails(getColorspaceName(), body.getPdfIndirectReference(),
                            null);
                    PdfArray array = new PdfArray(PdfName.PATTERN);
                    array.add(PdfName.DEVICEGRAY);
                    addToBody(array, patternColorspaceGRAY.getIndirectReference());
                }
                return patternColorspaceGRAY;
            case ExtendedColor.TYPE_SEPARATION: {
                ColorDetails details = addSimple(((SpotColor) color).getPdfSpotColor());
                ColorDetails patternDetails = documentSpotPatterns.get(details);
                if (patternDetails == null) {
                    patternDetails = new ColorDetails(getColorspaceName(), body.getPdfIndirectReference(), null);
                    PdfArray array = new PdfArray(PdfName.PATTERN);
                    array.add(details.getIndirectReference());
                    addToBody(array, patternDetails.getIndirectReference());
                    documentSpotPatterns.put(details, patternDetails);
                }
                return patternDetails;
            }
            default:
                throw new RuntimeException(MessageLocalization.getComposedMessage("invalid.color.type"));
            }
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    //  [M3] Images

    /**
     * Use this method to get the strictImageSequence status.
     * @return value of property strictImageSequence
     */
    public boolean isStrictImageSequence() {
        return pdf.isStrictImageSequence();
    }

    /**
     * Use this method to set the image sequence, so that it follows
     * the text in strict order (or not).
     * @param strictImageSequence new value of property strictImageSequence
     *
     */
    public void setStrictImageSequence(final boolean strictImageSequence) {
        pdf.setStrictImageSequence(strictImageSequence);
    }

    /**
     * Use this method to clear text wrapping around images (if applicable).
     * @throws DocumentException
     */
    public void clearTextWrap() throws DocumentException {
        pdf.clearTextWrap();
    }

    /** Dictionary, containing all the images of the PDF document */
    protected PdfDictionary imageDictionary = new PdfDictionary();

    /** This is the list with all the images in the document. */
    private final HashMap<Long, PdfName> images = new HashMap<Long, PdfName>();

    /**
     * Use this method to adds an image to the document
     * but not to the page resources. It is used with
     * templates and <CODE>Document.add(Image)</CODE>.
     * Use this method only if you know what you're doing!
     * @param image the <CODE>Image</CODE> to add
     * @return the name of the image added
     * @throws PdfException on error
     * @throws DocumentException on error
     */
    public PdfName addDirectImageSimple(final Image image) throws PdfException, DocumentException {
        return addDirectImageSimple(image, null);
    }

    /**
     * Adds an image to the document but not to the page resources.
     * It is used with templates and <CODE>Document.add(Image)</CODE>.
     * Use this method only if you know what you're doing!
     * @param image the <CODE>Image</CODE> to add
     * @param fixedRef the reference to used. It may be <CODE>null</CODE>,
     * a <CODE>PdfIndirectReference</CODE> or a <CODE>PRIndirectReference</CODE>.
     * @return the name of the image added
     * @throws PdfException on error
     * @throws DocumentException on error
     */
    public PdfName addDirectImageSimple(final Image image, final PdfIndirectReference fixedRef)
            throws PdfException, DocumentException {
        PdfName name;
        // if the images is already added, just retrieve the name
        if (images.containsKey(image.getMySerialId())) {
            name = images.get(image.getMySerialId());
        }
        // if it's a new image, add it to the document
        else {
            if (image.isImgTemplate()) {
                name = new PdfName("img" + images.size());
                if (image instanceof ImgWMF) {
                    try {
                        ImgWMF wmf = (ImgWMF) image;
                        wmf.readWMF(PdfTemplate.createTemplate(this, 0, 0));
                    } catch (Exception e) {
                        throw new DocumentException(e);
                    }
                }
            } else {
                PdfIndirectReference dref = image.getDirectReference();
                if (dref != null) {
                    PdfName rname = new PdfName("img" + images.size());
                    images.put(image.getMySerialId(), rname);
                    imageDictionary.put(rname, dref);
                    return rname;
                }
                Image maskImage = image.getImageMask();
                PdfIndirectReference maskRef = null;
                if (maskImage != null) {
                    PdfName mname = images.get(maskImage.getMySerialId());
                    maskRef = getImageReference(mname);
                }
                PdfImage i = new PdfImage(image, "img" + images.size(), maskRef);
                if (image instanceof ImgJBIG2) {
                    byte[] globals = ((ImgJBIG2) image).getGlobalBytes();
                    if (globals != null) {
                        PdfDictionary decodeparms = new PdfDictionary();
                        decodeparms.put(PdfName.JBIG2GLOBALS, getReferenceJBIG2Globals(globals));
                        i.put(PdfName.DECODEPARMS, decodeparms);
                    }
                }
                if (image.hasICCProfile()) {
                    PdfICCBased icc = new PdfICCBased(image.getICCProfile(), image.getCompressionLevel());
                    PdfIndirectReference iccRef = add(icc);
                    PdfArray iccArray = new PdfArray();
                    iccArray.add(PdfName.ICCBASED);
                    iccArray.add(iccRef);
                    PdfArray colorspace = i.getAsArray(PdfName.COLORSPACE);
                    if (colorspace != null) {
                        if (colorspace.size() > 1 && PdfName.INDEXED.equals(colorspace.getPdfObject(0)))
                            colorspace.set(1, iccArray);
                        else
                            i.put(PdfName.COLORSPACE, iccArray);
                    } else
                        i.put(PdfName.COLORSPACE, iccArray);
                }
                add(i, fixedRef);
                name = i.name();
            }
            images.put(image.getMySerialId(), name);
        }
        return name;
    }

    /**
     * Writes a <CODE>PdfImage</CODE> to the outputstream.
     *
     * @param pdfImage the image to be added
     * @param fixedRef the IndirectReference, may be null then a new indirect reference is returned
     * @return a <CODE>PdfIndirectReference</CODE> to the encapsulated image
     * @throws PdfException when a document isn't open yet, or has been closed
     */

    PdfIndirectReference add(final PdfImage pdfImage, PdfIndirectReference fixedRef) throws PdfException {
        if (!imageDictionary.contains(pdfImage.name())) {
            PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_IMAGE, pdfImage);
            if (fixedRef instanceof PRIndirectReference) {
                PRIndirectReference r2 = (PRIndirectReference) fixedRef;
                fixedRef = new PdfIndirectReference(0,
                        getNewObjectNumber(r2.getReader(), r2.getNumber(), r2.getGeneration()));
            }
            try {
                if (fixedRef == null)
                    fixedRef = addToBody(pdfImage).getIndirectReference();
                else
                    addToBody(pdfImage, fixedRef);
            } catch (IOException ioe) {
                throw new ExceptionConverter(ioe);
            }
            imageDictionary.put(pdfImage.name(), fixedRef);
            return fixedRef;
        }
        return (PdfIndirectReference) imageDictionary.get(pdfImage.name());
    }

    /**
     * return the <CODE>PdfIndirectReference</CODE> to the image with a given name.
     *
     * @param name the name of the image
     * @return a <CODE>PdfIndirectReference</CODE>
     */

    PdfIndirectReference getImageReference(final PdfName name) {
        return (PdfIndirectReference) imageDictionary.get(name);
    }

    protected PdfIndirectReference add(final PdfICCBased icc) {
        PdfIndirectObject object;
        try {
            object = addToBody(icc);
        } catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
        return object.getIndirectReference();
    }

    /**
     * A HashSet with Stream objects containing JBIG2 Globals
     * @since 2.1.5
     */
    protected HashMap<PdfStream, PdfIndirectReference> JBIG2Globals = new HashMap<PdfStream, PdfIndirectReference>();

    /**
     * Gets an indirect reference to a JBIG2 Globals stream.
     * Adds the stream if it hasn't already been added to the writer.
    * @param   content a byte array that may already been added to the writer inside a stream object.
     * @return the PdfIndirectReference of the stream
     * @since  2.1.5
     */
    protected PdfIndirectReference getReferenceJBIG2Globals(final byte[] content) {
        if (content == null)
            return null;
        for (PdfStream stream : JBIG2Globals.keySet()) {
            if (Arrays.equals(content, stream.getBytes())) {
                return JBIG2Globals.get(stream);
            }
        }
        PdfStream stream = new PdfStream(content);
        PdfIndirectObject ref;
        try {
            ref = addToBody(stream);
        } catch (IOException e) {
            return null;
        }
        JBIG2Globals.put(stream, ref.getIndirectReference());
        return ref.getIndirectReference();
    }

    //  [F12] tagged PDF
    /**
     * A flag indicating the presence of structure elements that contain user properties attributes.
     */
    private boolean userProperties;

    /**
     * Gets the flag indicating the presence of structure elements that contain user properties attributes.
     * @return the user properties flag
     */
    public boolean isUserProperties() {
        return this.userProperties;
    }

    /**
     * Sets the flag indicating the presence of structure elements that contain user properties attributes.
     * @param userProperties the user properties flag
     */
    public void setUserProperties(final boolean userProperties) {
        this.userProperties = userProperties;
    }

    /**
     * Holds value of property RGBTranparency.
     */
    private boolean rgbTransparencyBlending;

    /**
     * Gets the transparency blending colorspace.
     * @return <code>true</code> if the transparency blending colorspace is RGB, <code>false</code>
     * if it is the default blending colorspace
     * @since 2.1.0
     */
    public boolean isRgbTransparencyBlending() {
        return this.rgbTransparencyBlending;
    }

    /**
     * Sets the transparency blending colorspace to RGB. The default blending colorspace is
     * CMYK and will result in faded colors in the screen and in printing. Calling this method
     * will return the RGB colors to what is expected. The RGB blending will be applied to all subsequent pages
     * until other value is set.
     * Note that this is a generic solution that may not work in all cases.
     * @param rgbTransparencyBlending <code>true</code> to set the transparency blending colorspace to RGB, <code>false</code>
     * to use the default blending colorspace
     * @since 2.1.0
     */
    public void setRgbTransparencyBlending(final boolean rgbTransparencyBlending) {
        this.rgbTransparencyBlending = rgbTransparencyBlending;
    }

    protected static void writeKeyInfo(OutputStream os) throws IOException {
        Version version = Version.getInstance();
        String k = version.getKey();
        if (k == null) {
            k = "iText";
        }
        os.write(getISOBytes(String.format("%%%s-%s\n", k, version.getRelease())));

    }

    protected TtfUnicodeWriter ttfUnicodeWriter = null;

    protected TtfUnicodeWriter getTtfUnicodeWriter() {
        if (ttfUnicodeWriter == null)
            ttfUnicodeWriter = new TtfUnicodeWriter(this);
        return ttfUnicodeWriter;
    }

    protected XmpWriter createXmpWriter(ByteArrayOutputStream baos, PdfDictionary info) throws IOException {
        return new XmpWriter(baos, info);
    }

    protected XmpWriter createXmpWriter(ByteArrayOutputStream baos, HashMap<String, String> info)
            throws IOException {
        return new XmpWriter(baos, info);
    }

    /**
     * A wrapper around PdfAnnotation constructor.
     * It is recommended to use this wrapper instead of direct constructor as this is a convenient way to override PdfAnnotation construction when needed.
     *
     * @param rect
     * @param subtype
     * @return
     */
    public PdfAnnotation createAnnotation(Rectangle rect, PdfName subtype) {
        PdfAnnotation a = new PdfAnnotation(this, rect);
        if (subtype != null)
            a.put(PdfName.SUBTYPE, subtype);
        return a;
    }

    /**
     * A wrapper around PdfAnnotation constructor.
     * It is recommended to use this wrapper instead of direct constructor as this is a convenient way to override PdfAnnotation construction when needed.
     *
     * @param llx
     * @param lly
     * @param urx
     * @param ury
     * @param title
     * @param content
     * @param subtype
     * @return
     */
    public PdfAnnotation createAnnotation(float llx, float lly, float urx, float ury, PdfString title,
            PdfString content, PdfName subtype) {
        PdfAnnotation a = new PdfAnnotation(this, llx, lly, urx, ury, title, content);
        if (subtype != null)
            a.put(PdfName.SUBTYPE, subtype);
        return a;
    }

    /**
     * A wrapper around PdfAnnotation constructor.
     * It is recommended to use this wrapper instead of direct constructor as this is a convenient way to override PdfAnnotation construction when needed.
     *
     * @param llx
     * @param lly
     * @param urx
     * @param ury
     * @param action
     * @param subtype
     * @return
     */
    public PdfAnnotation createAnnotation(float llx, float lly, float urx, float ury, PdfAction action,
            PdfName subtype) {
        PdfAnnotation a = new PdfAnnotation(this, llx, lly, urx, ury, action);
        if (subtype != null)
            a.put(PdfName.SUBTYPE, subtype);
        return a;
    }

    public static void checkPdfIsoConformance(PdfWriter writer, int key, Object obj1) {
        if (writer != null)
            writer.checkPdfIsoConformance(key, obj1);
    }

    public void checkPdfIsoConformance(int key, Object obj1) {
        pdfIsoConformance.checkPdfIsoConformance(key, obj1);
    }

    private void completeInfoDictionary(PdfDictionary info) {
        if (isPdfX()) {
            if (info.get(PdfName.GTS_PDFXVERSION) == null) {
                if (((PdfXConformanceImp) pdfIsoConformance).isPdfX1A2001()) {
                    info.put(PdfName.GTS_PDFXVERSION, new PdfString("PDF/X-1:2001"));
                    info.put(new PdfName("GTS_PDFXConformance"), new PdfString("PDF/X-1a:2001"));
                } else if (((PdfXConformanceImp) pdfIsoConformance).isPdfX32002())
                    info.put(PdfName.GTS_PDFXVERSION, new PdfString("PDF/X-3:2002"));
            }
            if (info.get(PdfName.TITLE) == null) {
                info.put(PdfName.TITLE, new PdfString("Pdf document"));
            }
            if (info.get(PdfName.CREATOR) == null) {
                info.put(PdfName.CREATOR, new PdfString("Unknown"));
            }
            if (info.get(PdfName.TRAPPED) == null) {
                info.put(PdfName.TRAPPED, new PdfName("False"));
            }
        }
    }

    private void completeExtraCatalog(PdfDictionary extraCatalog) {
        if (isPdfX()) {
            if (extraCatalog.get(PdfName.OUTPUTINTENTS) == null) {
                PdfDictionary out = new PdfDictionary(PdfName.OUTPUTINTENT);
                out.put(PdfName.OUTPUTCONDITION, new PdfString("SWOP CGATS TR 001-1995"));
                out.put(PdfName.OUTPUTCONDITIONIDENTIFIER, new PdfString("CGATS TR 001"));
                out.put(PdfName.REGISTRYNAME, new PdfString("http://www.color.org"));
                out.put(PdfName.INFO, new PdfString(""));
                out.put(PdfName.S, PdfName.GTS_PDFX);
                extraCatalog.put(PdfName.OUTPUTINTENTS, new PdfArray(out));
            }
        }
    }

    static private final List<PdfName> standardStructElems_1_4 = Arrays.asList(PdfName.DOCUMENT, PdfName.PART,
            PdfName.ART, PdfName.SECT, PdfName.DIV, PdfName.BLOCKQUOTE, PdfName.CAPTION, PdfName.TOC, PdfName.TOCI,
            PdfName.INDEX, PdfName.NONSTRUCT, PdfName.PRIVATE, PdfName.P, PdfName.H, PdfName.H1, PdfName.H2,
            PdfName.H3, PdfName.H4, PdfName.H5, PdfName.H6, PdfName.L, PdfName.LBL, PdfName.LI, PdfName.LBODY,
            PdfName.TABLE, PdfName.TR, PdfName.TH, PdfName.TD, PdfName.SPAN, PdfName.QUOTE, PdfName.NOTE,
            PdfName.REFERENCE, PdfName.BIBENTRY, PdfName.CODE, PdfName.LINK, PdfName.FIGURE, PdfName.FORMULA,
            PdfName.FORM);

    static private final List<PdfName> standardStructElems_1_7 = Arrays.asList(PdfName.DOCUMENT, PdfName.PART,
            PdfName.ART, PdfName.SECT, PdfName.DIV, PdfName.BLOCKQUOTE, PdfName.CAPTION, PdfName.TOC, PdfName.TOCI,
            PdfName.INDEX, PdfName.NONSTRUCT, PdfName.PRIVATE, PdfName.P, PdfName.H, PdfName.H1, PdfName.H2,
            PdfName.H3, PdfName.H4, PdfName.H5, PdfName.H6, PdfName.L, PdfName.LBL, PdfName.LI, PdfName.LBODY,
            PdfName.TABLE, PdfName.TR, PdfName.TH, PdfName.TD, PdfName.THEAD, PdfName.TBODY, PdfName.TFOOT,
            PdfName.SPAN, PdfName.QUOTE, PdfName.NOTE, PdfName.REFERENCE, PdfName.BIBENTRY, PdfName.CODE,
            PdfName.LINK, PdfName.ANNOT, PdfName.RUBY, PdfName.RB, PdfName.RT, PdfName.RP, PdfName.WARICHU,
            PdfName.WT, PdfName.WP, PdfName.FIGURE, PdfName.FORMULA, PdfName.FORM);

    /**
     * Gets the list of the standard structure element names (roles).
     * @return
     */
    public List<PdfName> getStandardStructElems() {
        if (pdf_version.getVersion() < VERSION_1_7) {
            return standardStructElems_1_4;
        } else {
            return standardStructElems_1_7;
        }
    }

    public void useExternalCacheForTagStructure(TempFileCache fileCache) {
        pdf.useExternalCache(fileCache);
    }

}