/*
 *
 * This file is part of the iText (R) project.
    Copyright (c) 1998-2022 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.awt.geom.AffineTransform;
import com.itextpdf.awt.geom.Point;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.ExceptionConverter;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.Version;
import com.itextpdf.text.error_messages.MessageLocalization;
import com.itextpdf.text.exceptions.BadPasswordException;
import com.itextpdf.text.io.RandomAccessSource;
import com.itextpdf.text.io.RandomAccessSourceFactory;
import com.itextpdf.text.log.Counter;
import com.itextpdf.text.log.CounterFactory;
import com.itextpdf.text.log.Logger;
import com.itextpdf.text.log.LoggerFactory;
import com.itextpdf.text.pdf.AcroFields.Item;
import com.itextpdf.text.pdf.collection.PdfCollection;
import com.itextpdf.text.pdf.interfaces.PdfViewerPreferences;
import com.itextpdf.text.pdf.internal.PdfIsoKeys;
import com.itextpdf.text.pdf.internal.PdfViewerPreferencesImp;
import com.itextpdf.text.xml.xmp.PdfProperties;
import com.itextpdf.text.xml.xmp.XmpBasicProperties;
import com.itextpdf.text.xml.xmp.XmpWriter;
import com.itextpdf.xmp.XMPException;
import com.itextpdf.xmp.XMPMeta;
import com.itextpdf.xmp.XMPMetaFactory;
import com.itextpdf.xmp.options.SerializeOptions;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

class PdfStamperImp extends PdfWriter {
    HashMap<PdfReader, IntHashtable> readers2intrefs = new HashMap<PdfReader, IntHashtable>();
    HashMap<PdfReader, RandomAccessFileOrArray> readers2file = new HashMap<PdfReader, RandomAccessFileOrArray>();
    protected RandomAccessFileOrArray file;
    PdfReader reader;
    IntHashtable myXref = new IntHashtable();
    /**
     * Integer(page number) -> PageStamp
     */
    HashMap<PdfDictionary, PageStamp> pagesToContent = new HashMap<PdfDictionary, PageStamp>();
    protected boolean closed = false;
    /**
     * Holds value of property rotateContents.
     */
    private boolean rotateContents = true;
    protected AcroFields acroFields;
    protected boolean flat = false;
    protected boolean flatFreeText = false;
    protected boolean flatannotations = false;
    protected int namePtr[] = {0};
    protected HashSet<String> partialFlattening = new HashSet<String>();
    protected boolean useVp = false;
    protected PdfViewerPreferencesImp viewerPreferences = new PdfViewerPreferencesImp();
    protected HashSet<PdfTemplate> fieldTemplates = new HashSet<PdfTemplate>();
    protected boolean fieldsAdded = false;
    protected int sigFlags = 0;
    protected boolean append;
    protected IntHashtable marked;
    protected int initialXrefSize;
    protected PdfAction openAction;
    protected HashMap<Object, PdfObject> namedDestinations = new HashMap<Object, PdfObject>();

    protected Counter COUNTER = CounterFactory.getCounter(PdfStamper.class);
    private Logger logger;

    protected Counter getCounter() {
        return COUNTER;
    }

    /* Flag which defines if PdfLayer objects from existing pdf have been already read.
     * If no new layers were registered and user didn't fetched layers explicitly via getPdfLayers() method
     * then original layers are never read - they are simply copied to the new document with whole original catalog. */
    private boolean originalLayersAreRead = false;

    //Hash map of standard fonts used in flattening of annotations to prevent fonts duplication
    private HashMap<String, PdfIndirectReference> builtInAnnotationFonts = new HashMap<String, PdfIndirectReference>();
    private static HashMap<String, String> fromShortToFullAnnotationFontNames = new HashMap<String, String>();

    static {
        fromShortToFullAnnotationFontNames.put("CoBO", BaseFont.COURIER_BOLDOBLIQUE);
        fromShortToFullAnnotationFontNames.put("CoBo", BaseFont.COURIER_BOLD);
        fromShortToFullAnnotationFontNames.put("CoOb", BaseFont.COURIER_OBLIQUE);
        fromShortToFullAnnotationFontNames.put("Cour", BaseFont.COURIER);
        fromShortToFullAnnotationFontNames.put("HeBO", BaseFont.HELVETICA_BOLDOBLIQUE);
        fromShortToFullAnnotationFontNames.put("HeBo", BaseFont.HELVETICA_BOLD);
        fromShortToFullAnnotationFontNames.put("HeOb", BaseFont.HELVETICA_OBLIQUE);
        fromShortToFullAnnotationFontNames.put("Helv", BaseFont.HELVETICA);
        fromShortToFullAnnotationFontNames.put("Symb", BaseFont.SYMBOL);
        fromShortToFullAnnotationFontNames.put("TiBI", BaseFont.TIMES_BOLDITALIC);
        fromShortToFullAnnotationFontNames.put("TiBo", BaseFont.TIMES_BOLD);
        fromShortToFullAnnotationFontNames.put("TiIt", BaseFont.TIMES_ITALIC);
        fromShortToFullAnnotationFontNames.put("TiRo", BaseFont.TIMES_ROMAN);
        fromShortToFullAnnotationFontNames.put("ZaDb", BaseFont.ZAPFDINGBATS);
    }

    private double[] DEFAULT_MATRIX = {1, 0, 0, 1, 0, 0};

    /**
     * Creates new PdfStamperImp.
     *
     * @param reader     the read PDF
     * @param os         the output destination
     * @param pdfVersion the new pdf version or '\0' to keep the same version as the original
     *                   document
     * @param append
     * @throws DocumentException on error
     * @throws IOException
     */
    protected PdfStamperImp(PdfReader reader, OutputStream os, char pdfVersion, boolean append) throws DocumentException, IOException {
        super(new PdfDocument(), os);
        this.logger = LoggerFactory.getLogger(PdfStamperImp.class);
        if (!reader.isOpenedWithFullPermissions())
            throw new BadPasswordException(MessageLocalization.getComposedMessage("pdfreader.not.opened.with.owner.password"));
        if (reader.isTampered())
            throw new DocumentException(MessageLocalization.getComposedMessage("the.original.document.was.reused.read.it.again.from.file"));
        reader.setTampered(true);
        this.reader = reader;
        file = reader.getSafeFile();
        this.append = append;
        if (reader.isEncrypted() && (append || PdfReader.unethicalreading)) {
            crypto = new PdfEncryption(reader.getDecrypt());
        }
        if (append) {
            if (reader.isRebuilt())
                throw new DocumentException(MessageLocalization.getComposedMessage("append.mode.requires.a.document.without.errors.even.if.recovery.was.possible"));
            pdf_version.setAppendmode(true);
            if ( pdfVersion == 0 ) {
                pdf_version.setPdfVersion(reader.getPdfVersion());
            } else {
                pdf_version.setPdfVersion(pdfVersion);
            }
            byte buf[] = new byte[8192];
            int n;
            while ((n = file.read(buf)) > 0)
                this.os.write(buf, 0, n);
            prevxref = reader.getLastXref();
            reader.setAppendable(true);
        } else {
            if (pdfVersion == 0)
                super.setPdfVersion(reader.getPdfVersion());
            else
                super.setPdfVersion(pdfVersion);
        }

        if (reader.isTagged()) {
            this.setTagged();
        }

        super.open();
        pdf.addWriter(this);
        if (append) {
            body.setRefnum(reader.getXrefSize());
            marked = new IntHashtable();
            if (reader.isNewXrefType())
                fullCompression = true;
            if (reader.isHybridXref())
                fullCompression = false;
        }
        initialXrefSize = reader.getXrefSize();
        readColorProfile();
    }

    protected void readColorProfile() {
        PdfObject outputIntents = reader.getCatalog().getAsArray(PdfName.OUTPUTINTENTS);
        if (outputIntents != null && ((PdfArray) outputIntents).size() > 0) {
            PdfStream iccProfileStream = null;
            for (int i = 0; i < ((PdfArray) outputIntents).size(); i++) {
                PdfDictionary outputIntentDictionary = ((PdfArray) outputIntents).getAsDict(i);
                if (outputIntentDictionary != null) {
                    iccProfileStream = outputIntentDictionary.getAsStream(PdfName.DESTOUTPUTPROFILE);
                    if (iccProfileStream != null)
                        break;
                }
            }

            if (iccProfileStream instanceof PRStream) {
                try {
                    colorProfile = ICC_Profile.getInstance(PdfReader.getStreamBytes((PRStream) iccProfileStream));
                } catch (IOException exc) {
                    throw new ExceptionConverter(exc);
                }
            }
        }
    }

    protected void setViewerPreferences() {
        reader.setViewerPreferences(viewerPreferences);
        markUsed(reader.getTrailer().get(PdfName.ROOT));
    }

    protected void close(Map<String, String> moreInfo) throws IOException {
        if (closed) {
            return;
        }
        if (useVp) {
            setViewerPreferences();
        }
        if (flat) {
            flatFields();
        }
        if (flatFreeText) {
            flatFreeTextFields();
        }
        if (flatannotations) {
            flattenAnnotations();
        }
        addFieldResources();
        PdfDictionary catalog = reader.getCatalog();
        getPdfVersion().addToCatalog(catalog);
        PdfDictionary acroForm = (PdfDictionary) PdfReader.getPdfObject(catalog.get(PdfName.ACROFORM), reader.getCatalog());
        if (acroFields != null && acroFields.getXfa().isChanged()) {
            markUsed(acroForm);
            if (!flat) {
                acroFields.getXfa().setXfa(this);
            }
        }
        if (sigFlags != 0) {
            if (acroForm != null) {
                acroForm.put(PdfName.SIGFLAGS, new PdfNumber(sigFlags));
                markUsed(acroForm);
                markUsed(catalog);
            }
        }
        closed = true;
        addSharedObjectsToBody();
        setOutlines();
        setJavaScript();
        addFileAttachments();
        // [C11] Output Intents
        if (extraCatalog != null) {
            catalog.mergeDifferent(extraCatalog);
        }
        if (openAction != null) {
            catalog.put(PdfName.OPENACTION, openAction);
        }
        if (pdf.pageLabels != null) {
            catalog.put(PdfName.PAGELABELS, pdf.pageLabels.getDictionary(this));
        }
        // OCG
        if (!documentOCG.isEmpty()) {
            fillOCProperties(false);
            PdfDictionary ocdict = catalog.getAsDict(PdfName.OCPROPERTIES);
            if (ocdict == null) {
                reader.getCatalog().put(PdfName.OCPROPERTIES, OCProperties);
            } else {
                ocdict.put(PdfName.OCGS, OCProperties.get(PdfName.OCGS));
                PdfDictionary ddict = ocdict.getAsDict(PdfName.D);
                if (ddict == null) {
                    ddict = new PdfDictionary();
                    ocdict.put(PdfName.D, ddict);
                }
                ddict.put(PdfName.ORDER, OCProperties.getAsDict(PdfName.D).get(PdfName.ORDER));
                ddict.put(PdfName.RBGROUPS, OCProperties.getAsDict(PdfName.D).get(PdfName.RBGROUPS));
                ddict.put(PdfName.OFF, OCProperties.getAsDict(PdfName.D).get(PdfName.OFF));
                ddict.put(PdfName.AS, OCProperties.getAsDict(PdfName.D).get(PdfName.AS));
            }
            PdfWriter.checkPdfIsoConformance(this, PdfIsoKeys.PDFISOKEY_LAYER, OCProperties);
        }
        // metadata
        int skipInfo = -1;
        PdfIndirectReference iInfo = reader.getTrailer().getAsIndirectObject(PdfName.INFO);
        if (iInfo != null) {
            skipInfo = iInfo.getNumber();
        }
        PdfDictionary oldInfo = reader.getTrailer().getAsDict(PdfName.INFO);
        String producer = null;
        if (oldInfo != null && oldInfo.get(PdfName.PRODUCER) != null) {
            producer = oldInfo.getAsString(PdfName.PRODUCER).toUnicodeString();
        }
        Version version = Version.getInstance();
        if (producer == null || version.getVersion().indexOf(version.getProduct()) == -1) {
            producer = version.getVersion();
        } else {
            int idx = producer.indexOf("; modified using");
            StringBuffer buf;
            if (idx == -1)
                buf = new StringBuffer(producer);
            else
                buf = new StringBuffer(producer.substring(0, idx));
            buf.append("; modified using ");
            buf.append(version.getVersion());
            producer = buf.toString();
        }
        PdfIndirectReference info = null;
        PdfDictionary newInfo = new PdfDictionary();
        if (oldInfo != null) {
            for (Object element : oldInfo.getKeys()) {
                PdfName key = (PdfName) element;
                PdfObject value = oldInfo.get(key);
                newInfo.put(key, value);
            }
        }
        if (moreInfo != null) {
            for (Map.Entry<String, String> entry : moreInfo.entrySet()) {
                String key = entry.getKey();
                PdfName keyName = new PdfName(key);
                String value = entry.getValue();
                if (value == null)
                    newInfo.remove(keyName);
                else
                    newInfo.put(keyName, new PdfString(value, PdfObject.TEXT_UNICODE));
            }
        }
        PdfDate date = new PdfDate();
        newInfo.put(PdfName.MODDATE, date);
        newInfo.put(PdfName.PRODUCER, new PdfString(producer, PdfObject.TEXT_UNICODE));
        if (append) {
            if (iInfo == null) {
                info = addToBody(newInfo, false).getIndirectReference();
            } else {
                info = addToBody(newInfo, iInfo.getNumber(), false).getIndirectReference();
            }
        } else {
            info = addToBody(newInfo, false).getIndirectReference();
        }
        // XMP
        byte[] altMetadata = null;
        PdfObject xmpo = PdfReader.getPdfObject(catalog.get(PdfName.METADATA));
        if (xmpo != null && xmpo.isStream()) {
            altMetadata = PdfReader.getStreamBytesRaw((PRStream) xmpo);
            PdfReader.killIndirect(catalog.get(PdfName.METADATA));
        }
        PdfStream xmp = null;
        if (xmpMetadata != null) {
            altMetadata = xmpMetadata;
        } else if (xmpWriter != null) {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                PdfProperties.setProducer(xmpWriter.getXmpMeta(), producer);
                XmpBasicProperties.setModDate(xmpWriter.getXmpMeta(), date.getW3CDate());
                XmpBasicProperties.setMetaDataDate(xmpWriter.getXmpMeta(), date.getW3CDate());
                xmpWriter.serialize(baos);
                xmpWriter.close();
                xmp = new PdfStream(baos.toByteArray());
            } catch (XMPException exc) {
                xmpWriter = null;
            }
        }
        if (xmp == null && altMetadata != null) {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                if (moreInfo == null || xmpMetadata != null) {
                    XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(altMetadata);

                    PdfProperties.setProducer(xmpMeta, producer);
                    XmpBasicProperties.setModDate(xmpMeta, date.getW3CDate());
                    XmpBasicProperties.setMetaDataDate(xmpMeta, date.getW3CDate());

                    SerializeOptions serializeOptions = new SerializeOptions();
                    serializeOptions.setPadding(2000);
                    XMPMetaFactory.serialize(xmpMeta, baos, serializeOptions);
                } else {
                    XmpWriter xmpw = createXmpWriter(baos, newInfo);
                    xmpw.close();
                }
                xmp = new PdfStream(baos.toByteArray());
            } catch (XMPException e) {
                xmp = new PdfStream(altMetadata);
            } catch (IOException e) {
                xmp = new PdfStream(altMetadata);
            }
        }
        if (xmp != null) {
            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);
            }
            if (append && xmpo != null) {
                body.add(xmp, xmpo.getIndRef());
            } else {
                catalog.put(PdfName.METADATA, body.add(xmp).getIndirectReference());
                markUsed(catalog);
            }
        }

        if (!namedDestinations.isEmpty())
            updateNamedDestinations();
        close(info, skipInfo);
    }

    protected void close(PdfIndirectReference info, int skipInfo) throws IOException {
        alterContents();
        int rootN = ((PRIndirectReference) reader.trailer.get(PdfName.ROOT)).getNumber();
        if (append) {
            int keys[] = marked.getKeys();
            for (int k = 0; k < keys.length; ++k) {
                int j = keys[k];
                PdfObject obj = reader.getPdfObjectRelease(j);
                if (obj != null && skipInfo != j && j < initialXrefSize) {
                    addToBody(obj, obj.getIndRef(), j != rootN);
                }
            }
            for (int k = initialXrefSize; k < reader.getXrefSize(); ++k) {
                PdfObject obj = reader.getPdfObject(k);
                if (obj != null) {
                    addToBody(obj, getNewObjectNumber(reader, k, 0));
                }
            }
        } else {
            for (int k = 1; k < reader.getXrefSize(); ++k) {
                PdfObject obj = reader.getPdfObjectRelease(k);
                if (obj != null && skipInfo != k) {
                    addToBody(obj, getNewObjectNumber(reader, k, 0), k != rootN);
                }
            }
        }

        PdfIndirectReference encryption = null;
        PdfObject fileID = null;
        if (crypto != null) {
            if (append) {
                encryption = reader.getCryptoRef();
            } else {
                PdfIndirectObject encryptionObject = addToBody(crypto.getEncryptionDictionary(), false);
                encryption = encryptionObject.getIndirectReference();
            }
            fileID = crypto.getFileID(true);
        } else {
            PdfArray IDs = reader.trailer.getAsArray(PdfName.ID);
            if (IDs != null && IDs.getAsString(0) != null) {
                fileID = PdfEncryption.createInfoId(IDs.getAsString(0).getBytes(), true);
            } else {
                fileID = PdfEncryption.createInfoId(PdfEncryption.createDocumentId(), true);
            }
        }
        PRIndirectReference iRoot = (PRIndirectReference) reader.trailer.get(PdfName.ROOT);
        PdfIndirectReference root = new PdfIndirectReference(0, getNewObjectNumber(reader, iRoot.getNumber(), 0));
        // write the cross-reference table of the body
        body.writeCrossReferenceTable(os, root, info, encryption, fileID, prevxref);
        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(),
                    root,
                    info,
                    encryption,
                    fileID, prevxref);
            trailer.toPdf(this, os);
        }
        os.flush();
        if (isCloseStream())
            os.close();
        getCounter().written(os.getCounter());
    }

    void applyRotation(PdfDictionary pageN, ByteBuffer out) {
        if (!rotateContents)
            return;
        Rectangle page = reader.getPageSizeWithRotation(pageN);
        int rotation = page.getRotation();
        switch (rotation) {
            case 90:
                out.append(PdfContents.ROTATE90);
                out.append(page.getTop());
                out.append(' ').append('0').append(PdfContents.ROTATEFINAL);
                break;
            case 180:
                out.append(PdfContents.ROTATE180);
                out.append(page.getRight());
                out.append(' ');
                out.append(page.getTop());
                out.append(PdfContents.ROTATEFINAL);
                break;
            case 270:
                out.append(PdfContents.ROTATE270);
                out.append('0').append(' ');
                out.append(page.getRight());
                out.append(PdfContents.ROTATEFINAL);
                break;
        }
    }

    protected void alterContents() throws IOException {
        for (Object element : pagesToContent.values()) {
            PageStamp ps = (PageStamp) element;
            PdfDictionary pageN = ps.pageN;
            markUsed(pageN);
            PdfArray ar = null;
            PdfObject content = PdfReader.getPdfObject(pageN.get(PdfName.CONTENTS), pageN);
            if (content == null) {
                ar = new PdfArray();
                pageN.put(PdfName.CONTENTS, ar);
            } else if (content.isArray()) {
                ar = new PdfArray((PdfArray) content);
                pageN.put(PdfName.CONTENTS, ar);
            } else if (content.isStream()) {
                ar = new PdfArray();
                ar.add(pageN.get(PdfName.CONTENTS));
                pageN.put(PdfName.CONTENTS, ar);
            } else {
                ar = new PdfArray();
                pageN.put(PdfName.CONTENTS, ar);
            }
            ByteBuffer out = new ByteBuffer();
            if (ps.under != null) {
                out.append(PdfContents.SAVESTATE);
                applyRotation(pageN, out);
                out.append(ps.under.getInternalBuffer());
                out.append(PdfContents.RESTORESTATE);
            }
            if (ps.over != null)
                out.append(PdfContents.SAVESTATE);
            PdfStream stream = new PdfStream(out.toByteArray());
            stream.flateCompress(compressionLevel);
            ar.addFirst(addToBody(stream).getIndirectReference());
            out.reset();
            if (ps.over != null) {
                out.append(' ');
                out.append(PdfContents.RESTORESTATE);
                ByteBuffer buf = ps.over.getInternalBuffer();
                out.append(buf.getBuffer(), 0, ps.replacePoint);
                out.append(PdfContents.SAVESTATE);
                applyRotation(pageN, out);
                out.append(buf.getBuffer(), ps.replacePoint, buf.size() - ps.replacePoint);
                out.append(PdfContents.RESTORESTATE);
                stream = new PdfStream(out.toByteArray());
                stream.flateCompress(compressionLevel);
                ar.add(addToBody(stream).getIndirectReference());
            }
            alterResources(ps);
        }
    }

    void alterResources(PageStamp ps) {
        ps.pageN.put(PdfName.RESOURCES, ps.pageResources.getResources());
    }

    @Override
    protected int getNewObjectNumber(PdfReader reader, int number, int generation) {
        IntHashtable ref = readers2intrefs.get(reader);
        if (ref != null) {
            int n = ref.get(number);
            if (n == 0) {
                n = getIndirectReferenceNumber();
                ref.put(number, n);
            }
            return n;
        }
        if (currentPdfReaderInstance == null) {
            if (append && number < initialXrefSize)
                return number;
            int n = myXref.get(number);
            if (n == 0) {
                n = getIndirectReferenceNumber();
                myXref.put(number, n);
            }
            return n;
        } else
            return currentPdfReaderInstance.getNewObjectNumber(number, generation);
    }

    @Override
    RandomAccessFileOrArray getReaderFile(PdfReader reader) {
        if (readers2intrefs.containsKey(reader)) {
            RandomAccessFileOrArray raf = readers2file.get(reader);
            if (raf != null)
                return raf;
            return reader.getSafeFile();
        }
        if (currentPdfReaderInstance == null)
            return file;
        else
            return currentPdfReaderInstance.getReaderFile();
    }

    /**
     * @param reader
     * @param openFile
     * @throws IOException
     */
    public void registerReader(PdfReader reader, boolean openFile) throws IOException {
        if (readers2intrefs.containsKey(reader))
            return;
        readers2intrefs.put(reader, new IntHashtable());
        if (openFile) {
            RandomAccessFileOrArray raf = reader.getSafeFile();
            readers2file.put(reader, raf);
            raf.reOpen();
        }
    }

    /**
     * @param reader
     */
    public void unRegisterReader(PdfReader reader) {
        if (!readers2intrefs.containsKey(reader))
            return;
        readers2intrefs.remove(reader);
        RandomAccessFileOrArray raf = readers2file.get(reader);
        if (raf == null)
            return;
        readers2file.remove(reader);
        try {
            raf.close();
        } catch (Exception e) {
        }
    }

    static void findAllObjects(PdfReader reader, PdfObject obj, IntHashtable hits) {
        if (obj == null)
            return;
        switch (obj.type()) {
            case PdfObject.INDIRECT:
                PRIndirectReference iref = (PRIndirectReference) obj;
                if (reader != iref.getReader())
                    return;
                if (hits.containsKey(iref.getNumber()))
                    return;
                hits.put(iref.getNumber(), 1);
                findAllObjects(reader, PdfReader.getPdfObject(obj), hits);
                return;
            case PdfObject.ARRAY:
                PdfArray a = (PdfArray) obj;
                for (int k = 0; k < a.size(); ++k) {
                    findAllObjects(reader, a.getPdfObject(k), hits);
                }
                return;
            case PdfObject.DICTIONARY:
            case PdfObject.STREAM:
                PdfDictionary dic = (PdfDictionary) obj;
                for (Object element : dic.getKeys()) {
                    PdfName name = (PdfName) element;
                    findAllObjects(reader, dic.get(name), hits);
                }
                return;
        }
    }

    /**
     * @param fdf
     * @throws IOException
     */
    public void addComments(FdfReader fdf) throws IOException {
        if (readers2intrefs.containsKey(fdf))
            return;
        PdfDictionary catalog = fdf.getCatalog();
        catalog = catalog.getAsDict(PdfName.FDF);
        if (catalog == null)
            return;
        PdfArray annots = catalog.getAsArray(PdfName.ANNOTS);
        if (annots == null || annots.size() == 0)
            return;
        registerReader(fdf, false);
        IntHashtable hits = new IntHashtable();
        HashMap<String, PdfObject> irt = new HashMap<String, PdfObject>();
        ArrayList<PdfObject> an = new ArrayList<PdfObject>();
        for (int k = 0; k < annots.size(); ++k) {
            PdfObject obj = annots.getPdfObject(k);
            PdfDictionary annot = (PdfDictionary) PdfReader.getPdfObject(obj);
            PdfNumber page = annot.getAsNumber(PdfName.PAGE);
            if (page == null || page.intValue() >= reader.getNumberOfPages())
                continue;
            findAllObjects(fdf, obj, hits);
            an.add(obj);
            if (obj.type() == PdfObject.INDIRECT) {
                PdfObject nm = PdfReader.getPdfObject(annot.get(PdfName.NM));
                if (nm != null && nm.type() == PdfObject.STRING)
                    irt.put(nm.toString(), obj);
            }
        }
        int arhits[] = hits.getKeys();
        for (int k = 0; k < arhits.length; ++k) {
            int n = arhits[k];
            PdfObject obj = fdf.getPdfObject(n);
            if (obj.type() == PdfObject.DICTIONARY) {
                PdfObject str = PdfReader.getPdfObject(((PdfDictionary) obj).get(PdfName.IRT));
                if (str != null && str.type() == PdfObject.STRING) {
                    PdfObject i = irt.get(str.toString());
                    if (i != null) {
                        PdfDictionary dic2 = new PdfDictionary();
                        dic2.merge((PdfDictionary) obj);
                        dic2.put(PdfName.IRT, i);
                        obj = dic2;
                    }
                }
            }
            addToBody(obj, getNewObjectNumber(fdf, n, 0));
        }
        for (int k = 0; k < an.size(); ++k) {
            PdfObject obj = an.get(k);
            PdfDictionary annot = (PdfDictionary) PdfReader.getPdfObject(obj);
            PdfNumber page = annot.getAsNumber(PdfName.PAGE);
            PdfDictionary dic = reader.getPageN(page.intValue() + 1);
            PdfArray annotsp = (PdfArray) PdfReader.getPdfObject(dic.get(PdfName.ANNOTS), dic);
            if (annotsp == null) {
                annotsp = new PdfArray();
                dic.put(PdfName.ANNOTS, annotsp);
                markUsed(dic);
            }
            markUsed(annotsp);
            annotsp.add(obj);
        }
    }

    PageStamp getPageStamp(int pageNum) {
        PdfDictionary pageN = reader.getPageN(pageNum);
        PageStamp ps = pagesToContent.get(pageN);
        if (ps == null) {
            ps = new PageStamp(this, reader, pageN);
            pagesToContent.put(pageN, ps);
        }
        ps.pageN.setIndRef(reader.getPageOrigRef(pageNum));
        return ps;
    }

    PdfContentByte getUnderContent(int pageNum) {
        if (pageNum < 1 || pageNum > reader.getNumberOfPages())
            return null;
        PageStamp ps = getPageStamp(pageNum);
        if (ps.under == null)
            ps.under = new StampContent(this, ps);
        return ps.under;
    }

    PdfContentByte getOverContent(int pageNum) {
        if (pageNum < 1 || pageNum > reader.getNumberOfPages())
            return null;
        PageStamp ps = getPageStamp(pageNum);
        if (ps.over == null)
            ps.over = new StampContent(this, ps);
        return ps.over;
    }

    void correctAcroFieldPages(int page) {
        if (acroFields == null)
            return;
        if (page > reader.getNumberOfPages())
            return;
        Map<String, Item> fields = acroFields.getFields();
        for (AcroFields.Item item : fields.values()) {
            for (int k = 0; k < item.size(); ++k) {
                int p = item.getPage(k).intValue();
                if (p >= page)
                    item.forcePage(k, p + 1);
            }
        }
    }

    private static void moveRectangle(PdfDictionary dic2, PdfReader r, int pageImported, PdfName key, String name) {
        Rectangle m = r.getBoxSize(pageImported, name);
        if (m == null)
            dic2.remove(key);
        else
            dic2.put(key, new PdfRectangle(m));
    }

    void replacePage(PdfReader r, int pageImported, int pageReplaced) {
        PdfDictionary pageN = reader.getPageN(pageReplaced);
        if (pagesToContent.containsKey(pageN))
            throw new IllegalStateException(MessageLocalization.getComposedMessage("this.page.cannot.be.replaced.new.content.was.already.added"));
        PdfImportedPage p = getImportedPage(r, pageImported);
        PdfDictionary dic2 = reader.getPageNRelease(pageReplaced);
        dic2.remove(PdfName.RESOURCES);
        dic2.remove(PdfName.CONTENTS);
        moveRectangle(dic2, r, pageImported, PdfName.MEDIABOX, "media");
        moveRectangle(dic2, r, pageImported, PdfName.CROPBOX, "crop");
        moveRectangle(dic2, r, pageImported, PdfName.TRIMBOX, "trim");
        moveRectangle(dic2, r, pageImported, PdfName.ARTBOX, "art");
        moveRectangle(dic2, r, pageImported, PdfName.BLEEDBOX, "bleed");
        dic2.put(PdfName.ROTATE, new PdfNumber(r.getPageRotation(pageImported)));
        PdfContentByte cb = getOverContent(pageReplaced);
        cb.addTemplate(p, 0, 0);
        PageStamp ps = pagesToContent.get(pageN);
        ps.replacePoint = ps.over.getInternalBuffer().size();
    }

    void insertPage(int pageNumber, Rectangle mediabox) {
        Rectangle media = new Rectangle(mediabox);
        int rotation = media.getRotation() % 360;
        PdfDictionary page = new PdfDictionary(PdfName.PAGE);
        page.put(PdfName.RESOURCES, new PdfDictionary());
        page.put(PdfName.ROTATE, new PdfNumber(rotation));
        page.put(PdfName.MEDIABOX, new PdfRectangle(media, rotation));
        PRIndirectReference pref = reader.addPdfObject(page);
        PdfDictionary parent;
        PRIndirectReference parentRef;
        if (pageNumber > reader.getNumberOfPages()) {
            PdfDictionary lastPage = reader.getPageNRelease(reader.getNumberOfPages());
            parentRef = (PRIndirectReference) lastPage.get(PdfName.PARENT);
            parentRef = new PRIndirectReference(reader, parentRef.getNumber());
            parent = (PdfDictionary) PdfReader.getPdfObject(parentRef);
            PdfArray kids = (PdfArray) PdfReader.getPdfObject(parent.get(PdfName.KIDS), parent);
            kids.add(pref);
            markUsed(kids);
            reader.pageRefs.insertPage(pageNumber, pref);
        } else {
            if (pageNumber < 1)
                pageNumber = 1;
            PdfDictionary firstPage = reader.getPageN(pageNumber);
            PRIndirectReference firstPageRef = reader.getPageOrigRef(pageNumber);
            reader.releasePage(pageNumber);
            parentRef = (PRIndirectReference) firstPage.get(PdfName.PARENT);
            parentRef = new PRIndirectReference(reader, parentRef.getNumber());
            parent = (PdfDictionary) PdfReader.getPdfObject(parentRef);
            PdfArray kids = (PdfArray) PdfReader.getPdfObject(parent.get(PdfName.KIDS), parent);
            int len = kids.size();
            int num = firstPageRef.getNumber();
            for (int k = 0; k < len; ++k) {
                PRIndirectReference cur = (PRIndirectReference) kids.getPdfObject(k);
                if (num == cur.getNumber()) {
                    kids.add(k, pref);
                    break;
                }
            }
            if (len == kids.size())
                throw new RuntimeException(MessageLocalization.getComposedMessage("internal.inconsistence"));
            markUsed(kids);
            reader.pageRefs.insertPage(pageNumber, pref);
            correctAcroFieldPages(pageNumber);
        }
        page.put(PdfName.PARENT, parentRef);
        while (parent != null) {
            markUsed(parent);
            PdfNumber count = (PdfNumber) PdfReader.getPdfObjectRelease(parent.get(PdfName.COUNT));
            parent.put(PdfName.COUNT, new PdfNumber(count.intValue() + 1));
            parent = parent.getAsDict(PdfName.PARENT);
        }
    }

    /**
     * Getter for property rotateContents.
     *
     * @return Value of property rotateContents.
     */
    boolean isRotateContents() {
        return this.rotateContents;
    }

    /**
     * Setter for property rotateContents.
     *
     * @param rotateContents New value of property rotateContents.
     */
    void setRotateContents(boolean rotateContents) {
        this.rotateContents = rotateContents;
    }

    boolean isContentWritten() {
        return body.size() > 1;
    }

    AcroFields getAcroFields() {
        if (acroFields == null) {
            acroFields = new AcroFields(reader, this);
        }
        return acroFields;
    }

    void setFormFlattening(boolean flat) {
        this.flat = flat;
    }

    void setFreeTextFlattening(boolean flat) {
        this.flatFreeText = flat;
    }

    boolean partialFormFlattening(String name) {
        getAcroFields();
        if (acroFields.getXfa().isXfaPresent())
            throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("partial.form.flattening.is.not.supported.with.xfa.forms"));
        if (!acroFields.getFields().containsKey(name))
            return false;
        partialFlattening.add(name);
        return true;
    }

    protected void flatFields() {
        if (append)
            throw new IllegalArgumentException(MessageLocalization.getComposedMessage("field.flattening.is.not.supported.in.append.mode"));
        getAcroFields();
        Map<String, Item> fields = acroFields.getFields();
        if (fieldsAdded && partialFlattening.isEmpty()) {
            for (String s : fields.keySet()) {
                partialFlattening.add(s);
            }
        }
        PdfDictionary acroForm = reader.getCatalog().getAsDict(PdfName.ACROFORM);
        PdfArray acroFds = null;
        if (acroForm != null) {
            acroFds = (PdfArray) PdfReader.getPdfObject(acroForm.get(PdfName.FIELDS), acroForm);
        }
        for (Map.Entry<String, Item> entry : fields.entrySet()) {
            String name = entry.getKey();
            if (!partialFlattening.isEmpty() && !partialFlattening.contains(name))
                continue;
            AcroFields.Item item = entry.getValue();
            for (int k = 0; k < item.size(); ++k) {
                PdfDictionary merged = item.getMerged(k);
                PdfNumber ff = merged.getAsNumber(PdfName.F);
                int flags = 0;
                if (ff != null)
                    flags = ff.intValue();
                int page = item.getPage(k).intValue();
                if (page < 1)
                    continue;
                PdfDictionary appDic = merged.getAsDict(PdfName.AP);
                PdfObject as_n = null;
                if (appDic != null) {
                    as_n = appDic.getDirectObject(PdfName.N);
                    if (as_n == null)
                        as_n = appDic.getAsStream(PdfName.N);
                    if (as_n == null)
                        as_n = appDic.getAsDict(PdfName.N);
                }
                //The rotation can be already applied if the appearance stream was written to document during setField method
                boolean applyRotation = false;
                if (acroFields.isGenerateAppearances()) {
                    if (appDic == null || as_n == null) {
                        try {
                            acroFields.regenerateField(name);
                            appDic = acroFields.getFieldItem(name).getMerged(k).getAsDict(PdfName.AP);
                        }
                        // if we can't create appearances for some reason, we'll just continue
                        catch (IOException e) {
                        } catch (DocumentException e) {
                        }
                    } else if (as_n.isStream()) {
                        PdfStream stream = (PdfStream) as_n;
                        PdfArray bbox = stream.getAsArray(PdfName.BBOX);
                        PdfArray rect = merged.getAsArray(PdfName.RECT);
                        if (bbox != null && rect != null) {
                            applyRotation = true;
                            float rectWidth = rect.getAsNumber(2).floatValue() - rect.getAsNumber(0).floatValue();
                            float bboxWidth = bbox.getAsNumber(2).floatValue() - bbox.getAsNumber(0).floatValue();
                            float rectHeight = rect.getAsNumber(3).floatValue() - rect.getAsNumber(1).floatValue();
                            float bboxHeight = bbox.getAsNumber(3).floatValue() - bbox.getAsNumber(1).floatValue();
                            //Take field rotation into account
                            double fieldRotation = 0;
                            if(merged.getAsDict(PdfName.MK) != null){
                                if(merged.getAsDict(PdfName.MK).get(PdfName.R) != null){
                                    fieldRotation = merged.getAsDict(PdfName.MK).getAsNumber(PdfName.R).floatValue();
                                }
                            }
                            //Cast to radians
                            fieldRotation = fieldRotation * Math.PI/180;
                            //Clamp to [-2*Pi, 2*Pi]
                            fieldRotation = fieldRotation%(2*Math.PI);

                            if(fieldRotation%Math.PI != 0){
                                float temp = rectWidth;
                                rectWidth = rectHeight;
                                rectHeight = temp;
                            }
                            float widthCoef = Math.abs(bboxWidth != 0 ? rectWidth / bboxWidth : Float.MAX_VALUE);
                            float heightCoef = Math.abs(bboxHeight != 0 ? rectHeight / bboxHeight : Float.MAX_VALUE);

                            //Update matrix entry. Any field rotations present here will be overwritten and handled when adding the template to the canvas?
                            NumberArray array = new NumberArray(widthCoef, 0, 0, heightCoef, 0, 0);
                            stream.put(PdfName.MATRIX, array);
                            markUsed(stream);
                        }
                    }
                } else if (appDic != null && as_n != null && (as_n.isStream() || as_n.isDictionary())) {
                    PdfArray bbox = ((PdfDictionary) as_n).getAsArray(PdfName.BBOX);
                    PdfArray rect = merged.getAsArray(PdfName.RECT);
                    if (bbox != null && rect != null) {
                        float widthDiff = (bbox.getAsNumber(2).floatValue() - bbox.getAsNumber(0).floatValue()) -
                                (rect.getAsNumber(2).floatValue() - rect.getAsNumber(0).floatValue());
                        float heightDiff = (bbox.getAsNumber(3).floatValue() - bbox.getAsNumber(1).floatValue()) -
                                (rect.getAsNumber(3).floatValue() - rect.getAsNumber(1).floatValue());
                        if (Math.abs(widthDiff) > 1 || Math.abs(heightDiff) > 1) {
                            try {
                                //simulate Adobe behavior.
                                acroFields.setGenerateAppearances(true);
                                acroFields.regenerateField(name);
                                acroFields.setGenerateAppearances(false);
                                appDic = acroFields.getFieldItem(name).getMerged(k).getAsDict(PdfName.AP);
                            }
                            // if we can't create appearances for some reason, we'll just continue
                            catch (IOException e) {
                            } catch (DocumentException e) {
                            }
                        }
                    }
                }

                if (appDic != null && (flags & PdfFormField.FLAGS_PRINT) != 0 && (flags & PdfFormField.FLAGS_HIDDEN) == 0) {
                    PdfObject obj = appDic.get(PdfName.N);
                    PdfAppearance app = null;
                    PdfObject objReal = PdfReader.getPdfObject(obj);
                    if (obj != null) {
                        if (obj instanceof PdfIndirectReference && !obj.isIndirect())
                            app = new PdfAppearance((PdfIndirectReference) obj);
                        else if (objReal instanceof PdfStream) {
                            ((PdfDictionary) objReal).put(PdfName.SUBTYPE, PdfName.FORM);
                            app = new PdfAppearance((PdfIndirectReference) obj);
                        } else {
                            if (objReal != null && objReal.isDictionary()) {
                                PdfName as = merged.getAsName(PdfName.AS);
                                if (as != null) {
                                    PdfIndirectReference iref = (PdfIndirectReference) ((PdfDictionary) objReal).get(as);
                                    if (iref != null) {
                                        app = new PdfAppearance(iref);
                                        if (iref.isIndirect()) {
                                            objReal = PdfReader.getPdfObject(iref);
                                            ((PdfDictionary) objReal).put(PdfName.SUBTYPE, PdfName.FORM);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if (app != null) {
                        Rectangle box = PdfReader.getNormalizedRectangle(merged.getAsArray(PdfName.RECT));
                        PdfContentByte cb = getOverContent(page);
                        cb.setLiteral("Q ");
                        if (applyRotation) {
                            /*
                            Apply field rotation
                             */
                            AffineTransform tf = new AffineTransform();
                            double fieldRotation = 0;
                            if (merged.getAsDict(PdfName.MK) != null) {
                                if (merged.getAsDict(PdfName.MK).get(PdfName.R) != null) {
                                    fieldRotation = merged.getAsDict(PdfName.MK).getAsNumber(PdfName.R).floatValue();
                                }
                            }
                            //Cast to radians
                            fieldRotation = fieldRotation * Math.PI / 180;
                            //Clamp to [-2*Pi, 2*Pi]
                            fieldRotation = fieldRotation % (2 * Math.PI);
                            //Calculate transformation matrix
                            tf = calculateTemplateTransformationMatrix(tf, fieldRotation, box);
                            cb.addTemplate(app, tf);
                        } else {
                            if(objReal instanceof PdfDictionary && ((PdfDictionary)objReal).getAsArray(PdfName.BBOX) != null) {
                                Rectangle bBox = PdfReader.getNormalizedRectangle((((PdfDictionary)objReal).getAsArray(PdfName.BBOX)));
                                cb.addTemplate(app, (box.getWidth() / bBox.getWidth()), 0, 0, (box.getHeight() / bBox.getHeight()), box.getLeft(), box.getBottom());
                            } else {
                                cb.addTemplate(app, box.getLeft(), box.getBottom());
                            }
                        }
                        cb.setLiteral("q ");
                    }
                }
                if (partialFlattening.isEmpty())
                    continue;
                PdfDictionary pageDic = reader.getPageN(page);
                PdfArray annots = pageDic.getAsArray(PdfName.ANNOTS);
                if (annots == null)
                    continue;
                for (int idx = 0; idx < annots.size(); ++idx) {
                    PdfObject ran = annots.getPdfObject(idx);
                    if (!ran.isIndirect())
                        continue;
                    PdfObject ran2 = item.getWidgetRef(k);
                    if (!ran2.isIndirect())
                        continue;
                    if (((PRIndirectReference) ran).getNumber() == ((PRIndirectReference) ran2).getNumber()) {
                        annots.remove(idx--);
                        PRIndirectReference wdref = (PRIndirectReference) ran2;
                        while (true) {
                            PdfDictionary wd = (PdfDictionary) PdfReader.getPdfObject(wdref);
                            PRIndirectReference parentRef = (PRIndirectReference) wd.get(PdfName.PARENT);
                            PdfReader.killIndirect(wdref);
                            if (parentRef == null) { // reached AcroForm
                                for (int fr = 0; fr < acroFds.size(); ++fr) {
                                    PdfObject h = acroFds.getPdfObject(fr);
                                    if (h.isIndirect() && ((PRIndirectReference) h).getNumber() == wdref.getNumber()) {
                                        acroFds.remove(fr);
                                        --fr;
                                    }
                                }
                                break;
                            }
                            PdfDictionary parent = (PdfDictionary) PdfReader.getPdfObject(parentRef);
                            PdfArray kids = parent.getAsArray(PdfName.KIDS);
                            for (int fr = 0; fr < kids.size(); ++fr) {
                                PdfObject h = kids.getPdfObject(fr);
                                if (h.isIndirect() && ((PRIndirectReference) h).getNumber() == wdref.getNumber()) {
                                    kids.remove(fr);
                                    --fr;
                                }
                            }
                            if (!kids.isEmpty())
                                break;
                            wdref = parentRef;
                        }
                    }
                }
                if (annots.isEmpty()) {
                    PdfReader.killIndirect(pageDic.get(PdfName.ANNOTS));
                    pageDic.remove(PdfName.ANNOTS);
                }
            }
        }
        if (!fieldsAdded && partialFlattening.isEmpty()) {
            for (int page = 1; page <= reader.getNumberOfPages(); ++page) {
                PdfDictionary pageDic = reader.getPageN(page);
                PdfArray annots = pageDic.getAsArray(PdfName.ANNOTS);
                if (annots == null)
                    continue;
                for (int idx = 0; idx < annots.size(); ++idx) {
                    PdfObject annoto = annots.getDirectObject(idx);
                    if (annoto instanceof PdfIndirectReference && !annoto.isIndirect())
                        continue;
                    if (!annoto.isDictionary() || PdfName.WIDGET.equals(((PdfDictionary) annoto).get(PdfName.SUBTYPE))) {
                        annots.remove(idx);
                        --idx;
                    }
                }
                if (annots.isEmpty()) {
                    PdfReader.killIndirect(pageDic.get(PdfName.ANNOTS));
                    pageDic.remove(PdfName.ANNOTS);
                }
            }
            eliminateAcroformObjects();
        }
    }

    void eliminateAcroformObjects() {
        PdfObject acro = reader.getCatalog().get(PdfName.ACROFORM);
        if (acro == null)
            return;
        PdfDictionary acrodic = (PdfDictionary) PdfReader.getPdfObject(acro);
        reader.killXref(acrodic.get(PdfName.XFA));
        acrodic.remove(PdfName.XFA);
        PdfObject iFields = acrodic.get(PdfName.FIELDS);
        if (iFields != null) {
            PdfDictionary kids = new PdfDictionary();
            kids.put(PdfName.KIDS, iFields);
            sweepKids(kids);
            PdfReader.killIndirect(iFields);
            acrodic.put(PdfName.FIELDS, new PdfArray());
        }
        acrodic.remove(PdfName.SIGFLAGS);
        acrodic.remove(PdfName.NEEDAPPEARANCES);
        acrodic.remove(PdfName.DR);
//        PdfReader.killIndirect(acro);
//        reader.getCatalog().remove(PdfName.ACROFORM);
    }

    private AffineTransform calculateTemplateTransformationMatrix(AffineTransform currentMatrix, double fieldRotation, Rectangle box) {
        AffineTransform templateTransform = new AffineTransform(currentMatrix);
        //Move to new origin
        double x = box.getLeft();
        double y = box.getBottom();
        if (fieldRotation % (Math.PI / 2) == 0 && fieldRotation % (3 * Math.PI / 2) != 0 && fieldRotation != 0) {
            x += box.getWidth();
        }
        if((fieldRotation%(3*Math.PI/2)==0 || fieldRotation%(Math.PI)==0) && fieldRotation != 0){
            y+=box.getHeight();
        }
        templateTransform.translate(x,y);
        //Apply fieldrotation
        templateTransform.rotate(fieldRotation);
        return templateTransform;
    }

    void sweepKids(PdfObject obj) {
        PdfObject oo = PdfReader.killIndirect(obj);
        if (oo == null || !oo.isDictionary())
            return;
        PdfDictionary dic = (PdfDictionary) oo;
        PdfArray kids = (PdfArray) PdfReader.killIndirect(dic.get(PdfName.KIDS));
        if (kids == null)
            return;
        for (int k = 0; k < kids.size(); ++k) {
            sweepKids(kids.getPdfObject(k));
        }
    }

    /**
     * If true, annotations with an appearance stream will be flattened.
     *
     * @param flatAnnotations boolean
     * @since 5.5.3
     */
    public void setFlatAnnotations(boolean flatAnnotations) {
        this.flatannotations = flatAnnotations;
    }

    /**
     * If setFlatAnnotations is set to true, iText will flatten all annotations with an appearance stream
     *
     * @since 5.5.3
     */
    protected void flattenAnnotations() {
        flattenAnnotations(false);
    }

    private void flattenAnnotations(boolean flattenFreeTextAnnotations) {
        if (append) {
            if (flattenFreeTextAnnotations) {
                throw new IllegalArgumentException(MessageLocalization.getComposedMessage("freetext.flattening.is.not.supported.in.append.mode"));
            } else {
                throw new IllegalArgumentException(MessageLocalization.getComposedMessage("annotation.flattening.is.not.supported.in.append.mode"));
            }
        }

        for (int page = 1; page <= reader.getNumberOfPages(); ++page) {
            PdfDictionary pageDic = reader.getPageN(page);
            PdfArray annots = pageDic.getAsArray(PdfName.ANNOTS);

            if (annots == null) {
                continue;
            }

            for (int idx = 0; idx < annots.size(); ++idx) {
                PdfObject annoto = annots.getDirectObject(idx);
                if (annoto instanceof PdfIndirectReference && !annoto.isIndirect())
                    continue;
                if (!(annoto instanceof PdfDictionary))
                    continue;
                PdfDictionary annDic = (PdfDictionary) annoto;
                final PdfObject subType = annDic.get(PdfName.SUBTYPE);
                if (flattenFreeTextAnnotations) {
                    if (! PdfName.FREETEXT.equals(subType)) {
                        continue;
                    }
                } else {
                    if (PdfName.WIDGET.equals(subType)) {
                        // skip widgets
                        continue;
                    }
                }

                PdfNumber ff = annDic.getAsNumber(PdfName.F);
                int flags = ff != null ? ff.intValue() : 0;

                if ((flags & PdfFormField.FLAGS_PRINT) != 0 && (flags & PdfFormField.FLAGS_HIDDEN) == 0) {
                    PdfObject obj1 = annDic.get(PdfName.AP);
                    if (obj1 == null)
                        continue;
                    PdfDictionary appDic = obj1 instanceof PdfIndirectReference ?
                            (PdfDictionary) PdfReader.getPdfObject(obj1) : (PdfDictionary) obj1;
                    PdfObject obj = appDic.get(PdfName.N);
                    PdfDictionary objDict = appDic.getAsStream(PdfName.N);
                    PdfAppearance app = null;
                    PdfObject objReal = PdfReader.getPdfObject(obj);

                    if (obj instanceof PdfIndirectReference && !obj.isIndirect()) {
                        app = new PdfAppearance((PdfIndirectReference) obj);
                    } else if (objReal instanceof PdfStream) {
                        ((PdfDictionary) objReal).put(PdfName.SUBTYPE, PdfName.FORM);
                        app = new PdfAppearance((PdfIndirectReference) obj);
                    } else {
                        if (objReal != null ) {
                            if (objReal.isDictionary()) {
                                PdfName as_p = appDic.getAsName(PdfName.AS);
                                if (as_p != null) {
                                    PdfIndirectReference iref = (PdfIndirectReference) ( (PdfDictionary) objReal ).get(as_p);
                                    if (iref != null) {
                                        app = new PdfAppearance(iref);
                                        if (iref.isIndirect()) {
                                            objReal = PdfReader.getPdfObject(iref);
                                            ( (PdfDictionary) objReal ).put(PdfName.SUBTYPE, PdfName.FORM);
                                        }
                                    }
                                }
                            }
                        } else {
                            if ( PdfName.FREETEXT.equals(subType) ) {
                                final PdfString defaultAppearancePdfString = annDic.getAsString(PdfName.DA);
                                if (defaultAppearancePdfString != null) {
                                    final PdfString freeTextContent = annDic.getAsString(PdfName.CONTENTS);
                                    final String defaultAppearanceString = defaultAppearancePdfString.toString();

                                    //It is not stated in spec, but acrobat seems to support standard font names in DA
                                    //So we need to check if the font is built-in and specify it explicitly.
                                    PdfIndirectReference fontReference = null;
                                    PdfName pdfFontName = null;
                                    try {
                                        RandomAccessSource source = new RandomAccessSourceFactory().createSource(defaultAppearancePdfString.getBytes());
                                        PdfContentParser ps = new PdfContentParser(new PRTokeniser(new RandomAccessFileOrArray(source)));
                                        ArrayList<PdfObject> operands = new ArrayList<PdfObject>();
                                        while (ps.parse(operands).size() > 0) {
                                            PdfLiteral operator = (PdfLiteral)operands.get(operands.size()-1);
                                            if (operator.toString().equals("Tf")) {
                                                pdfFontName = (PdfName) operands.get(0);
                                                String fontName = pdfFontName.toString().substring(1);
                                                String fullName = fromShortToFullAnnotationFontNames.get(fontName);
                                                if (fullName == null) {
                                                    fullName = fontName;
                                                }
                                                fontReference = builtInAnnotationFonts.get(fullName);
                                                if (fontReference == null) {
                                                    PdfDictionary dic = BaseFont.createBuiltInFontDictionary(fullName);
                                                    if (dic != null) {
                                                        fontReference = addToBody(dic).getIndirectReference();
                                                        builtInAnnotationFonts.put(fullName, fontReference);
                                                    }
                                                }
                                            }
                                        }
                                    } catch (Exception any) {
                                        logger.warn(MessageLocalization.getComposedMessage("error.resolving.freetext.font"));
                                        break;
                                    }

                                    app = new PdfAppearance(this);
                                    // it is unclear from spec were referenced from DA font should be (since annotations doesn't have DR), so in case it not built-in
                                    // quickly and naively flattening the freetext annotation
                                    if (fontReference != null) {
                                        app.getPageResources().addFont(pdfFontName, fontReference);
                                    }
                                    app.saveState();
                                    app.beginText();
                                    app.setLiteral(defaultAppearanceString);
                                    app.setLiteral("(" + freeTextContent.toString() + ") Tj\n");
                                    app.endText();
                                    app.restoreState();
                                } else {
                                    // The DA entry is required for free text annotations
                                    // Not throwing an exception as we don't want to stop the flow, result is that this annotation won't be flattened.
                                    this.logger.warn(MessageLocalization.getComposedMessage("freetext.annotation.doesnt.contain.da"));
                                }
                            } else {
                                this.logger.warn(MessageLocalization.getComposedMessage("annotation.type.not.supported.flattening"));
                            }
                        }
                    }
                    if (app != null) {
                        Rectangle rect = PdfReader.getNormalizedRectangle(annDic.getAsArray(PdfName.RECT));
                        Rectangle bBox = null;

                        if ( objDict != null ) {
                            bBox = PdfReader.getNormalizedRectangle((objDict.getAsArray(PdfName.BBOX)));
                        } else {
                            bBox = new Rectangle(0,0, rect.getWidth(), rect.getHeight());
                            app.setBoundingBox(bBox);
                        }

                        PdfContentByte cb = getOverContent(page);
                        cb.setLiteral("Q ");
                        if (objDict != null && objDict.getAsArray(PdfName.MATRIX) != null &&
                                !Arrays.equals(DEFAULT_MATRIX, objDict.getAsArray(PdfName.MATRIX).asDoubleArray())) {
                            double[] matrix = objDict.getAsArray(PdfName.MATRIX).asDoubleArray();
                            Rectangle transformBBox = transformBBoxByMatrix(bBox, matrix);
                            cb.addTemplate(app, (rect.getWidth() / transformBBox.getWidth()), 0, 0, (rect.getHeight() / transformBBox.getHeight()), rect.getLeft(), rect.getBottom());
                        } else {
                            //Correct for offset origins in the BBox, similar to how Adobe will flatten.
                            float heightCorrection = - bBox.getBottom();
                            float widthCorrection = - bBox.getLeft();
                            //Changed so that when the annotation has a difference scale than the xObject in the appearance dictionary, the image is consistent between
                            //the input and the flattened document.  When the annotation is rotated or skewed, it will still be flattened incorrectly.
                            cb.addTemplate(app, (rect.getWidth() / bBox.getWidth()), 0, 0, (rect.getHeight() / bBox.getHeight()), rect.getLeft() + widthCorrection, rect.getBottom()+heightCorrection);
                        }
                        cb.setLiteral("q ");

                        annots.remove(idx);
                        --idx;
                    }
                }
            }

            if (annots.isEmpty()) {
                PdfReader.killIndirect(pageDic.get(PdfName.ANNOTS));
                pageDic.remove(PdfName.ANNOTS);
            }
        }
    }

    /*
    * The transformation BBOX between two coordinate systems can be
    * represented by a 3-by-3 transformation matrix and create new BBOX based min(x,y) and
     * max(x,y) coordinate pairs
    * */
    private Rectangle transformBBoxByMatrix(Rectangle bBox, double[] matrix) {
        List xArr = new ArrayList();
        List yArr = new ArrayList();
        Point p1 = transformPoint(bBox.getLeft(), bBox.getBottom(), matrix);
        xArr.add(p1.x);
        yArr.add(p1.y);
        Point p2 = transformPoint(bBox.getRight(), bBox.getTop(), matrix);
        xArr.add(p2.x);
        yArr.add(p2.y);
        Point p3 = transformPoint(bBox.getLeft(), bBox.getTop(), matrix);
        xArr.add(p3.x);
        yArr.add(p3.y);
        Point p4 = transformPoint(bBox.getRight(), bBox.getBottom(), matrix);
        xArr.add(p4.x);
        yArr.add(p4.y);

        return new Rectangle(((Double) Collections.min(xArr)).floatValue(),
                ((Double) Collections.min(yArr)).floatValue(),
                ((Double) Collections.max(xArr)).floatValue(),
                ((Double) Collections.max(yArr)).floatValue());
    }

    /*
    *  transform point by algorithm
    *  x′ = a*x + c×y + e
    *  y' = b*x + d*y + f
    *  [ a b c d e f ] transformation matrix values
    * */
    private Point transformPoint(double x, double y, double[] matrix) {
        Point point = new Point();
        point.x = matrix[0] * x + matrix[2] * y + matrix[4];
        point.y = matrix[1] * x + matrix[3] * y + matrix[5];
        return point;
    }

    protected void flatFreeTextFields() {
        flattenAnnotations(true);
    }

    /**
     * @see com.itextpdf.text.pdf.PdfWriter#getPageReference(int)
     */
    @Override
    public PdfIndirectReference getPageReference(int page) {
        PdfIndirectReference ref = reader.getPageOrigRef(page);
        if (ref == null)
            throw new IllegalArgumentException(MessageLocalization.getComposedMessage("invalid.page.number.1", page));
        return ref;
    }

    /**
     * @see com.itextpdf.text.pdf.PdfWriter#addAnnotation(com.itextpdf.text.pdf.PdfAnnotation)
     */
    @Override
    public void addAnnotation(PdfAnnotation annot) {
        throw new RuntimeException(MessageLocalization.getComposedMessage("unsupported.in.this.context.use.pdfstamper.addannotation"));
    }

    void addDocumentField(PdfIndirectReference ref) {
        PdfDictionary catalog = reader.getCatalog();
        PdfDictionary acroForm = (PdfDictionary) PdfReader.getPdfObject(catalog.get(PdfName.ACROFORM), catalog);
        if (acroForm == null) {
            acroForm = new PdfDictionary();
            catalog.put(PdfName.ACROFORM, acroForm);
            markUsed(catalog);
        }
        PdfArray fields = (PdfArray) PdfReader.getPdfObject(acroForm.get(PdfName.FIELDS), acroForm);
        if (fields == null) {
            fields = new PdfArray();
            acroForm.put(PdfName.FIELDS, fields);
            markUsed(acroForm);
        }
        if (!acroForm.contains(PdfName.DA)) {
            acroForm.put(PdfName.DA, new PdfString("/Helv 0 Tf 0 g "));
            markUsed(acroForm);
        }
        fields.add(ref);
        markUsed(fields);
    }

    protected void addFieldResources() throws IOException {
        if (fieldTemplates.isEmpty())
            return;
        PdfDictionary catalog = reader.getCatalog();
        PdfDictionary acroForm = (PdfDictionary) PdfReader.getPdfObject(catalog.get(PdfName.ACROFORM), catalog);
        if (acroForm == null) {
            acroForm = new PdfDictionary();
            catalog.put(PdfName.ACROFORM, acroForm);
            markUsed(catalog);
        }
        PdfDictionary dr = (PdfDictionary) PdfReader.getPdfObject(acroForm.get(PdfName.DR), acroForm);
        if (dr == null) {
            dr = new PdfDictionary();
            acroForm.put(PdfName.DR, dr);
            markUsed(acroForm);
        }
        markUsed(dr);
        for (PdfTemplate template : fieldTemplates) {
            PdfFormField.mergeResources(dr, (PdfDictionary) template.getResources(), this);
        }
        // if (dr.get(PdfName.ENCODING) == null) dr.put(PdfName.ENCODING, PdfName.WIN_ANSI_ENCODING);
        PdfDictionary fonts = dr.getAsDict(PdfName.FONT);
        if (fonts == null) {
            fonts = new PdfDictionary();
            dr.put(PdfName.FONT, fonts);
        }
        if (!fonts.contains(PdfName.HELV)) {
            PdfDictionary dic = new PdfDictionary(PdfName.FONT);
            dic.put(PdfName.BASEFONT, PdfName.HELVETICA);
            dic.put(PdfName.ENCODING, PdfName.WIN_ANSI_ENCODING);
            dic.put(PdfName.NAME, PdfName.HELV);
            dic.put(PdfName.SUBTYPE, PdfName.TYPE1);
            fonts.put(PdfName.HELV, addToBody(dic).getIndirectReference());
        }
        if (!fonts.contains(PdfName.ZADB)) {
            PdfDictionary dic = new PdfDictionary(PdfName.FONT);
            dic.put(PdfName.BASEFONT, PdfName.ZAPFDINGBATS);
            dic.put(PdfName.NAME, PdfName.ZADB);
            dic.put(PdfName.SUBTYPE, PdfName.TYPE1);
            fonts.put(PdfName.ZADB, addToBody(dic).getIndirectReference());
        }
        if (acroForm.get(PdfName.DA) == null) {
            acroForm.put(PdfName.DA, new PdfString("/Helv 0 Tf 0 g "));
            markUsed(acroForm);
        }
    }

    void expandFields(PdfFormField field, ArrayList<PdfAnnotation> allAnnots) {
        allAnnots.add(field);
        ArrayList<PdfFormField> kids = field.getKids();
        if (kids != null) {
            for (int k = 0; k < kids.size(); ++k)
                expandFields(kids.get(k), allAnnots);
        }
    }

    void addAnnotation(PdfAnnotation annot, PdfDictionary pageN) {
        try {
            ArrayList<PdfAnnotation> allAnnots = new ArrayList<PdfAnnotation>();
            if (annot.isForm()) {
                fieldsAdded = true;
                getAcroFields();
                PdfFormField field = (PdfFormField) annot;
                if (field.getParent() != null)
                    return;
                expandFields(field, allAnnots);
            } else
                allAnnots.add(annot);
            for (int k = 0; k < allAnnots.size(); ++k) {
                annot = allAnnots.get(k);
                if (annot.getPlaceInPage() > 0)
                    pageN = reader.getPageN(annot.getPlaceInPage());
                if (annot.isForm()) {
                    if (!annot.isUsed()) {
                        HashSet<PdfTemplate> templates = annot.getTemplates();
                        if (templates != null)
                            fieldTemplates.addAll(templates);
                    }
                    PdfFormField field = (PdfFormField) annot;
                    if (field.getParent() == null)
                        addDocumentField(field.getIndirectReference());
                }
                if (annot.isAnnotation()) {
                    PdfObject pdfobj = PdfReader.getPdfObject(pageN.get(PdfName.ANNOTS), pageN);
                    PdfArray annots = null;
                    if (pdfobj == null || !pdfobj.isArray()) {
                        annots = new PdfArray();
                        pageN.put(PdfName.ANNOTS, annots);
                        markUsed(pageN);
                    } else
                        annots = (PdfArray) pdfobj;
                    annots.add(annot.getIndirectReference());
                    markUsed(annots);
                    if (!annot.isUsed()) {
                        PdfRectangle rect = (PdfRectangle) annot.get(PdfName.RECT);
                        if (rect != null && (rect.left() != 0 || rect.right() != 0 || rect.top() != 0 || rect.bottom() != 0)) {
                            int rotation = reader.getPageRotation(pageN);
                            Rectangle pageSize = reader.getPageSizeWithRotation(pageN);
                            switch (rotation) {
                                case 90:
                                    annot.put(PdfName.RECT, new PdfRectangle(
                                            pageSize.getTop() - rect.top(),
                                            rect.right(),
                                            pageSize.getTop() - rect.bottom(),
                                            rect.left()));
                                    break;
                                case 180:
                                    annot.put(PdfName.RECT, new PdfRectangle(
                                            pageSize.getRight() - rect.left(),
                                            pageSize.getTop() - rect.bottom(),
                                            pageSize.getRight() - rect.right(),
                                            pageSize.getTop() - rect.top()));
                                    break;
                                case 270:
                                    annot.put(PdfName.RECT, new PdfRectangle(
                                            rect.bottom(),
                                            pageSize.getRight() - rect.left(),
                                            rect.top(),
                                            pageSize.getRight() - rect.right()));
                                    break;
                            }
                        }
                    }
                }
                if (!annot.isUsed()) {
                    annot.setUsed();
                    addToBody(annot, annot.getIndirectReference());
                }
            }
        } catch (IOException e) {
            throw new ExceptionConverter(e);
        }
    }

    @Override
    void addAnnotation(PdfAnnotation annot, int page) {
        if (annot.isAnnotation())
            annot.setPage(page);
        addAnnotation(annot, reader.getPageN(page));
    }

    private void outlineTravel(PRIndirectReference outline) {
        while (outline != null) {
            PdfDictionary outlineR = (PdfDictionary) PdfReader.getPdfObjectRelease(outline);
            PRIndirectReference first = (PRIndirectReference) outlineR.get(PdfName.FIRST);
            if (first != null) {
                outlineTravel(first);
            }
            PdfReader.killIndirect(outlineR.get(PdfName.DEST));
            PdfReader.killIndirect(outlineR.get(PdfName.A));
            PdfReader.killIndirect(outline);
            outline = (PRIndirectReference) outlineR.get(PdfName.NEXT);
        }
    }

    void deleteOutlines() {
        PdfDictionary catalog = reader.getCatalog();
        PdfObject obj = catalog.get(PdfName.OUTLINES);
        if (obj == null)
            return;
        if (obj instanceof PRIndirectReference) {
            PRIndirectReference outlines = (PRIndirectReference) obj;
            outlineTravel(outlines);
            PdfReader.killIndirect(outlines);
        }
        catalog.remove(PdfName.OUTLINES);
        markUsed(catalog);
    }

    protected void setJavaScript() throws IOException {
        HashMap<String, PdfObject> djs = pdf.getDocumentLevelJS();
        if (djs.isEmpty())
            return;
        PdfDictionary catalog = reader.getCatalog();
        PdfDictionary names = (PdfDictionary) PdfReader.getPdfObject(catalog.get(PdfName.NAMES), catalog);
        if (names == null) {
            names = new PdfDictionary();
            catalog.put(PdfName.NAMES, names);
            markUsed(catalog);
        }
        markUsed(names);
        PdfDictionary tree = PdfNameTree.writeTree(djs, this);
        names.put(PdfName.JAVASCRIPT, addToBody(tree).getIndirectReference());
    }

    protected void addFileAttachments() throws IOException {
        HashMap<String, PdfObject> fs = pdf.getDocumentFileAttachment();
        if (fs.isEmpty())
            return;
        PdfDictionary catalog = reader.getCatalog();
        PdfDictionary names = (PdfDictionary) PdfReader.getPdfObject(catalog.get(PdfName.NAMES), catalog);
        if (names == null) {
            names = new PdfDictionary();
            catalog.put(PdfName.NAMES, names);
            markUsed(catalog);
        }
        markUsed(names);
        HashMap<String, PdfObject> old = PdfNameTree.readTree((PdfDictionary) PdfReader.getPdfObjectRelease(names.get(PdfName.EMBEDDEDFILES)));
        for (Map.Entry<String, PdfObject> entry : fs.entrySet()) {
            String name = entry.getKey();
            int k = 0;
            StringBuilder nn = new StringBuilder(name);
            while (old.containsKey(nn.toString())) {
                ++k;
                nn.append(" ").append(k);
            }
            old.put(nn.toString(), entry.getValue());
        }
        PdfDictionary tree = PdfNameTree.writeTree(old, this);
        // Remove old EmbeddedFiles object if preset
        PdfObject oldEmbeddedFiles = names.get(PdfName.EMBEDDEDFILES);
        if (oldEmbeddedFiles != null) {
            PdfReader.killIndirect(oldEmbeddedFiles);
        }

        // Add new EmbeddedFiles object
        names.put(PdfName.EMBEDDEDFILES, addToBody(tree).getIndirectReference());
    }

    /**
     * Adds or replaces the Collection Dictionary in the Catalog.
     *
     * @param collection the new collection dictionary.
     */
    void makePackage(PdfCollection collection) {
        PdfDictionary catalog = reader.getCatalog();
        catalog.put(PdfName.COLLECTION, collection);
    }

    protected void setOutlines() throws IOException {
        if (newBookmarks == null)
            return;
        deleteOutlines();
        if (newBookmarks.isEmpty())
            return;
        PdfDictionary catalog = reader.getCatalog();
        boolean namedAsNames = catalog.get(PdfName.DESTS) != null;
        writeOutlines(catalog, namedAsNames);
        markUsed(catalog);
    }

    /**
     * Sets the viewer preferences.
     *
     * @param preferences the viewer preferences
     * @see PdfWriter#setViewerPreferences(int)
     */
    @Override
    public void setViewerPreferences(int preferences) {
        useVp = true;
        this.viewerPreferences.setViewerPreferences(preferences);
    }

    /**
     * Adds a viewer preference
     *
     * @param key   a key for a viewer preference
     * @param value the value for the viewer preference
     * @see PdfViewerPreferences#addViewerPreference
     */
    @Override
    public void addViewerPreference(PdfName key, PdfObject value) {
        useVp = true;
        this.viewerPreferences.addViewerPreference(key, value);
    }

    /**
     * Set the signature flags.
     *
     * @param f the flags. This flags are ORed with current ones
     */
    @Override
    public void setSigFlags(int f) {
        sigFlags |= f;
    }

    /**
     * Always throws an <code>UnsupportedOperationException</code>.
     *
     * @param actionType ignore
     * @param action     ignore
     * @throws PdfException ignore
     * @see PdfStamper#setPageAction(PdfName, PdfAction, int)
     */
    @Override
    public void setPageAction(PdfName actionType, PdfAction action) throws PdfException {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.setpageaction.pdfname.actiontype.pdfaction.action.int.page"));
    }

    /**
     * Sets the open and close page additional action.
     *
     * @param actionType the action type. It can be <CODE>PdfWriter.PAGE_OPEN</CODE>
     *                   or <CODE>PdfWriter.PAGE_CLOSE</CODE>
     * @param action     the action to perform
     * @param page       the page where the action will be applied. The first page is 1
     * @throws PdfException if the action type is invalid
     */
    void setPageAction(PdfName actionType, PdfAction action, int page) throws PdfException {
        if (!actionType.equals(PAGE_OPEN) && !actionType.equals(PAGE_CLOSE))
            throw new PdfException(MessageLocalization.getComposedMessage("invalid.page.additional.action.type.1", actionType.toString()));
        PdfDictionary pg = reader.getPageN(page);
        PdfDictionary aa = (PdfDictionary) PdfReader.getPdfObject(pg.get(PdfName.AA), pg);
        if (aa == null) {
            aa = new PdfDictionary();
            pg.put(PdfName.AA, aa);
            markUsed(pg);
        }
        aa.put(actionType, action);
        markUsed(aa);
    }

    /**
     * Always throws an <code>UnsupportedOperationException</code>.
     *
     * @param seconds ignore
     */
    @Override
    public void setDuration(int seconds) {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.setpageaction.pdfname.actiontype.pdfaction.action.int.page"));
    }

    /**
     * Always throws an <code>UnsupportedOperationException</code>.
     *
     * @param transition ignore
     */
    @Override
    public void setTransition(PdfTransition transition) {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.setpageaction.pdfname.actiontype.pdfaction.action.int.page"));
    }

    /**
     * Sets the display duration for the page (for presentations)
     *
     * @param seconds the number of seconds to display the page. A negative value removes the entry
     * @param page    the page where the duration will be applied. The first page is 1
     */
    void setDuration(int seconds, int page) {
        PdfDictionary pg = reader.getPageN(page);
        if (seconds < 0)
            pg.remove(PdfName.DUR);
        else
            pg.put(PdfName.DUR, new PdfNumber(seconds));
        markUsed(pg);
    }

    /**
     * Sets the transition for the page
     *
     * @param transition the transition object. A <code>null</code> removes the transition
     * @param page       the page where the transition will be applied. The first page is 1
     */
    void setTransition(PdfTransition transition, int page) {
        PdfDictionary pg = reader.getPageN(page);
        if (transition == null)
            pg.remove(PdfName.TRANS);
        else
            pg.put(PdfName.TRANS, transition.getTransitionDictionary());
        markUsed(pg);
    }

    protected void markUsed(PdfObject obj) {
        if (append && obj != null) {
            PRIndirectReference ref = null;
            if (obj.type() == PdfObject.INDIRECT)
                ref = (PRIndirectReference) obj;
            else
                ref = obj.getIndRef();
            if (ref != null)
                marked.put(ref.getNumber(), 1);
        }
    }

    protected void markUsed(int num) {
        if (append)
            marked.put(num, 1);
    }

    /**
     * Getter for property append.
     *
     * @return Value of property append.
     */
    boolean isAppend() {
        return append;
    }

    public PdfReader getPdfReader() {
        return this.reader;
    }

    /**
     * Additional-actions defining the actions to be taken in
     * response to various trigger events affecting the document
     * as a whole. The actions types allowed are: <CODE>DOCUMENT_CLOSE</CODE>,
     * <CODE>WILL_SAVE</CODE>, <CODE>DID_SAVE</CODE>, <CODE>WILL_PRINT</CODE>
     * and <CODE>DID_PRINT</CODE>.
     *
     * @param actionType the action type
     * @param action     the action to execute in response to the trigger
     * @throws PdfException on invalid action type
     */
    @Override
    public void setAdditionalAction(PdfName actionType, PdfAction action) throws PdfException {
        if (!(actionType.equals(DOCUMENT_CLOSE) ||
                actionType.equals(WILL_SAVE) ||
                actionType.equals(DID_SAVE) ||
                actionType.equals(WILL_PRINT) ||
                actionType.equals(DID_PRINT))) {
            throw new PdfException(MessageLocalization.getComposedMessage("invalid.additional.action.type.1", actionType.toString()));
        }
        PdfDictionary aa = reader.getCatalog().getAsDict(PdfName.AA);
        if (aa == null) {
            if (action == null)
                return;
            aa = new PdfDictionary();
            reader.getCatalog().put(PdfName.AA, aa);
        }
        markUsed(aa);
        if (action == null)
            aa.remove(actionType);
        else
            aa.put(actionType, action);
    }

    /**
     * @see com.itextpdf.text.pdf.PdfWriter#setOpenAction(com.itextpdf.text.pdf.PdfAction)
     */
    @Override
    public void setOpenAction(PdfAction action) {
        openAction = action;
    }

    /**
     * @see com.itextpdf.text.pdf.PdfWriter#setOpenAction(java.lang.String)
     */
    @Override
    public void setOpenAction(String name) {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("open.actions.by.name.are.not.supported"));
    }

    /**
     * @see com.itextpdf.text.pdf.PdfWriter#setThumbnail(com.itextpdf.text.Image)
     */
    @Override
    public void setThumbnail(com.itextpdf.text.Image image) {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.pdfstamper.setthumbnail"));
    }

    void setThumbnail(Image image, int page) throws PdfException, DocumentException {
        PdfIndirectReference thumb = getImageReference(addDirectImageSimple(image));
        reader.resetReleasePage();
        PdfDictionary dic = reader.getPageN(page);
        dic.put(PdfName.THUMB, thumb);
        reader.resetReleasePage();
    }

    @Override
    public PdfContentByte getDirectContentUnder() {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.pdfstamper.getundercontent.or.pdfstamper.getovercontent"));
    }

    @Override
    public PdfContentByte getDirectContent() {
        throw new UnsupportedOperationException(MessageLocalization.getComposedMessage("use.pdfstamper.getundercontent.or.pdfstamper.getovercontent"));
    }

    /**
     * Reads the OCProperties dictionary from the catalog of the existing document
     * and fills the documentOCG, documentOCGorder and OCGRadioGroup variables in PdfWriter.
     * Note that the original OCProperties of the existing document can contain more information.
     *
     * @since 2.1.2
     */
    protected void readOCProperties() {
        if (!documentOCG.isEmpty()) {
            return;
        }
        PdfDictionary dict = reader.getCatalog().getAsDict(PdfName.OCPROPERTIES);
        if (dict == null) {
            return;
        }
        PdfArray ocgs = dict.getAsArray(PdfName.OCGS);
        if (ocgs == null) {
            ocgs = new PdfArray();
            dict.put(PdfName.OCGS, ocgs);
        }
        PdfIndirectReference ref;
        PdfLayer layer;
        HashMap<String, PdfLayer> ocgmap = new HashMap<String, PdfLayer>();
        for (Iterator<PdfObject> i = ocgs.listIterator(); i.hasNext(); ) {
            ref = (PdfIndirectReference) i.next();
            layer = new PdfLayer(null);
            layer.setRef(ref);
            layer.setOnPanel(false);
            layer.merge((PdfDictionary) PdfReader.getPdfObject(ref));
            ocgmap.put(ref.toString(), layer);
        }
        PdfDictionary d = dict.getAsDict(PdfName.D);
        PdfArray off = d.getAsArray(PdfName.OFF);
        if (off != null) {
            for (Iterator<PdfObject> i = off.listIterator(); i.hasNext(); ) {
                ref = (PdfIndirectReference) i.next();
                layer = ocgmap.get(ref.toString());
                layer.setOn(false);
            }
        }
        PdfArray order = d.getAsArray(PdfName.ORDER);
        if (order != null) {
            addOrder(null, order, ocgmap);
        }
        documentOCG.addAll(ocgmap.values());
        OCGRadioGroup = d.getAsArray(PdfName.RBGROUPS);
        if (OCGRadioGroup == null)
            OCGRadioGroup = new PdfArray();
        OCGLocked = d.getAsArray(PdfName.LOCKED);
        if (OCGLocked == null)
            OCGLocked = new PdfArray();
    }

    /**
     * Recursive method to reconstruct the documentOCGorder variable in the writer.
     *
     * @param parent a parent PdfLayer (can be null)
     * @param arr    an array possibly containing children for the parent PdfLayer
     * @param ocgmap a HashMap with indirect reference Strings as keys and PdfLayer objects as values.
     * @since 2.1.2
     */
    private void addOrder(PdfLayer parent, PdfArray arr, Map<String, PdfLayer> ocgmap) {
        PdfObject obj;
        PdfLayer layer;
        for (int i = 0; i < arr.size(); i++) {
            obj = arr.getPdfObject(i);
            if (obj.isIndirect()) {
                layer = ocgmap.get(obj.toString());
                if (layer != null) {
                    layer.setOnPanel(true);
                    registerLayer(layer);
                    if (parent != null) {
                        parent.addChild(layer);
                    }
                    if (arr.size() > i + 1 && arr.getPdfObject(i + 1).isArray()) {
                        i++;
                        addOrder(layer, (PdfArray) arr.getPdfObject(i), ocgmap);
                    }
                }
            } else if (obj.isArray()) {
                PdfArray sub = (PdfArray) obj;
                if (sub.isEmpty()) return;
                obj = sub.getPdfObject(0);
                if (obj.isString()) {
                    layer = new PdfLayer(obj.toString());
                    layer.setOnPanel(true);
                    registerLayer(layer);
                    if (parent != null) {
                        parent.addChild(layer);
                    }
                    PdfArray array = new PdfArray();
                    for (Iterator<PdfObject> j = sub.listIterator(); j.hasNext(); ) {
                        array.add(j.next());
                    }
                    addOrder(layer, array, ocgmap);
                } else {
                    addOrder(parent, (PdfArray) obj, ocgmap);
                }
            }
        }
    }

    /**
     * Gets the PdfLayer objects in an existing document as a Map
     * with the names/titles of the layers as keys.
     *
     * @return a Map with all the PdfLayers in the document (and the name/title of the layer as key)
     * @since 2.1.2
     */
    public Map<String, PdfLayer> getPdfLayers() {
        if (!originalLayersAreRead) {
            originalLayersAreRead = true;
            readOCProperties();
        }
        HashMap<String, PdfLayer> map = new HashMap<String, PdfLayer>();
        PdfLayer layer;
        String key;
        for (PdfOCG pdfOCG : documentOCG) {
            layer = (PdfLayer) pdfOCG;
            if (layer.getTitle() == null) {
                key = layer.getAsString(PdfName.NAME).toString();
            } else {
                key = layer.getTitle();
            }
            if (map.containsKey(key)) {
                int seq = 2;
                String tmp = key + "(" + seq + ")";
                while (map.containsKey(tmp)) {
                    seq++;
                    tmp = key + "(" + seq + ")";
                }
                key = tmp;
            }
            map.put(key, layer);
        }
        return map;
    }

    @Override
    void registerLayer(final PdfOCG layer) {
        if (!originalLayersAreRead) {
            originalLayersAreRead = true;
            readOCProperties();
        }
        super.registerLayer(layer);
    }

    public void createXmpMetadata() {
        try {
            xmpWriter = createXmpWriter(null, reader.getInfo());
            xmpMetadata = null;
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    protected HashMap<Object, PdfObject> getNamedDestinations() {
        return namedDestinations;
    }

    protected void updateNamedDestinations() throws IOException {

        PdfDictionary dic = reader.getCatalog().getAsDict(PdfName.NAMES);
        if (dic != null)
            dic = dic.getAsDict(PdfName.DESTS);
        if (dic == null) {
            dic = reader.getCatalog().getAsDict(PdfName.DESTS);
        }
        if (dic == null) {
            dic = new PdfDictionary();
            PdfDictionary dests = new PdfDictionary();
            dic.put(PdfName.NAMES, new PdfArray());
            dests.put(PdfName.DESTS, dic);
            reader.getCatalog().put(PdfName.NAMES, dests);
        }

        PdfArray names = getLastChildInNameTree(dic);

        for (Object name : namedDestinations.keySet()) {
            names.add(new PdfString(name.toString()));
            names.add(addToBody(namedDestinations.get(name), getPdfIndirectReference()).getIndirectReference());
        }
    }

    private PdfArray getLastChildInNameTree(PdfDictionary dic) {

        PdfArray names;

        PdfArray childNode = dic.getAsArray(PdfName.KIDS);
        if (childNode != null) {
            PdfDictionary lastKid = childNode.getAsDict(childNode.size() - 1);
            names = getLastChildInNameTree(lastKid);
        } else {
            names = dic.getAsArray(PdfName.NAMES);
        }

        return names;
    }

    static class PageStamp {

        PdfDictionary pageN;
        StampContent under;
        StampContent over;
        PageResources pageResources;
        int replacePoint = 0;

        PageStamp(PdfStamperImp stamper, PdfReader reader, PdfDictionary pageN) {
            this.pageN = pageN;
            pageResources = new PageResources();
            PdfDictionary resources = pageN.getAsDict(PdfName.RESOURCES);
            pageResources.setOriginalResources(resources, stamper.namePtr);
        }
    }
}
