PdfDocument.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    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 <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf;

import com.itextpdf.commons.actions.EventManager;
import com.itextpdf.commons.actions.IEventHandler;
import com.itextpdf.commons.actions.confirmations.ConfirmEvent;
import com.itextpdf.commons.actions.confirmations.EventConfirmationType;
import com.itextpdf.commons.actions.data.ProductData;
import com.itextpdf.commons.actions.sequence.SequenceId;
import com.itextpdf.commons.utils.DIContainer;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.io.source.ByteUtils;
import com.itextpdf.io.source.RandomAccessFileOrArray;
import com.itextpdf.kernel.actions.data.ITextCoreProductData;
import com.itextpdf.kernel.actions.events.FlushPdfDocumentEvent;
import com.itextpdf.kernel.actions.events.ITextCoreProductEvent;
import com.itextpdf.kernel.colors.Color;
import com.itextpdf.kernel.exceptions.BadPasswordException;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.MemoryLimitsAwareException;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.numbering.EnglishAlphabetNumbering;
import com.itextpdf.kernel.numbering.RomanNumbering;
import com.itextpdf.kernel.pdf.PdfReader.StrictnessLevel;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.kernel.pdf.collection.PdfCollection;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEvent;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEventHandler;
import com.itextpdf.kernel.pdf.event.PdfDocumentEvent;
import com.itextpdf.kernel.pdf.filespec.PdfEncryptedPayloadFileSpecFactory;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.kernel.pdf.navigation.PdfDestination;
import com.itextpdf.kernel.pdf.statistics.NumberOfPagesStatisticsEvent;
import com.itextpdf.kernel.pdf.statistics.SizeOfPdfStatisticsEvent;
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
import com.itextpdf.kernel.validation.IValidationContext;
import com.itextpdf.kernel.validation.ValidationContainer;
import com.itextpdf.kernel.validation.context.CryptoValidationContext;
import com.itextpdf.kernel.validation.context.PdfDocumentValidationContext;
import com.itextpdf.kernel.xmp.PdfConst;
import com.itextpdf.kernel.xmp.XMPConst;
import com.itextpdf.kernel.xmp.XMPException;
import com.itextpdf.kernel.xmp.XMPMeta;
import com.itextpdf.kernel.xmp.XMPMetaFactory;
import com.itextpdf.kernel.xmp.options.SerializeOptions;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Main enter point to work with PDF document.
 */
public class PdfDocument implements Closeable {
    private static final PdfName[] PDF_NAMES_TO_REMOVE_FROM_ORIGINAL_TRAILER = new PdfName[] {
            PdfName.Encrypt,
            PdfName.Size,
            PdfName.Prev,
            PdfName.Root,
            PdfName.Info,
            PdfName.ID,
            PdfName.XRefStm,
            PdfName.AuthCode
    };

    private static final Logger LOGGER = LoggerFactory.getLogger(PdfDocument.class);

    protected final StampingProperties properties;
    /**
     * List of indirect objects used in the document.
     */
    final PdfXrefTable xref = new PdfXrefTable();
    private final Map<PdfIndirectReference, PdfFont> documentFonts = new HashMap<>();
    private final Set<IEventHandler> documentHandlers = new LinkedHashSet<>();
    private final SequenceId documentId;
    /**
     * To be adjusted destinations.
     * Key - originating page on the source document
     * Value - a hashmap of Parent pdf objects and destinations to be updated
     */
    private final List<DestinationMutationInfo> pendingDestinationMutations =
            new ArrayList<DestinationMutationInfo>();
    /**
     * PdfWriter associated with the document.
     * Not null if document opened either in writing or stamping mode.
     */
    protected PdfWriter writer = null;

    /**
     * PdfReader associated with the document.
     * Not null if document is opened either in reading or stamping mode.
     */
    protected PdfReader reader = null;
    /**
     * Document catalog.
     */
    protected PdfCatalog catalog = null;
    /**
     * Document trailed.
     */
    protected PdfDictionary trailer = null;
    /**
     * Document version.
     */
    protected PdfVersion pdfVersion = PdfVersion.PDF_1_7;
    protected PdfConformance pdfConformance = PdfConformance.PDF_NONE_CONFORMANCE;
    protected FingerPrint fingerPrint;
    protected SerializeOptions serializeOptions = new SerializeOptions();
    protected PdfStructTreeRoot structTreeRoot;
    protected int structParentIndex = -1;
    protected boolean closeReader = true;
    protected boolean closeWriter = true;
    protected boolean isClosing = false;
    protected boolean closed = false;
    /**
     * flag determines whether to write unused objects to result document
     */
    protected boolean flushUnusedObjects = false;
    protected TagStructureContext tagStructureContext;

    protected DocumentInfoHelper documentInfoHelper = new DocumentInfoHelper();
    protected DefaultFontStrategy defaultFontStrategy = new DefaultFontStrategy(this);
    protected IPdfPageFactory pdfPageFactory = new PdfPageFactory();
    /**
     * Cache of already serialized objects from this document for smart mode.
     */
    Map<PdfIndirectReference, byte[]> serializedObjectsCache = new HashMap<>();
    /**
     * Handler which will be used for decompression of pdf streams.
     */
    MemoryLimitsAwareHandler memoryLimitsAwareHandler = null;
    /**
     * Default page size.
     * New page by default will be created with this size.
     */
    private PageSize defaultPageSize = PageSize.DEFAULT;
    /**
     * The original (first) id when the document is read initially.
     */
    private PdfString originalDocumentId;
    /**
     * The original modified (second) id when the document is read initially.
     */
    private PdfString modifiedDocumentId;
    private EncryptedEmbeddedStreamsHandler encryptedEmbeddedStreamsHandler;
    /**
     * Document info.
     */
    private PdfDocumentInfo info = null;
    /**
     * XMP Metadata bytes for the document.
     */
    private byte[] xmpMetadataBytes = null;
    /**
     * XMP Metadata which is used to prevent bytes deserialization for a few times on the same bytes.
     */
    private XMPMeta xmpMetadata = null;


    private final DIContainer diContainer = new DIContainer();

    /**
     * Open PDF document in reading mode.
     *
     * @param reader PDF reader.
     */
    public PdfDocument(PdfReader reader) {
        this(reader, new DocumentProperties());
    }

    /**
     * Open PDF document in reading mode.
     *
     * @param reader     PDF reader.
     * @param properties document properties
     */
    public PdfDocument(PdfReader reader, DocumentProperties properties) {
        if (reader == null) {
            throw new IllegalArgumentException("The reader in PdfDocument constructor can not be null.");
        }
        documentId = new SequenceId();
        this.reader = reader;
        this.properties = new StampingProperties(properties);
        open(null);
    }

    /**
     * Open PDF document in writing mode.
     * Document has no pages when initialized.
     *
     * @param writer PDF writer
     */
    public PdfDocument(PdfWriter writer) {
        this(writer, new DocumentProperties());
    }

    /**
     * Open PDF document in writing mode.
     * Document has no pages when initialized.
     *
     * @param writer     PDF writer
     * @param properties document properties
     */
    public PdfDocument(PdfWriter writer, DocumentProperties properties) {
        if (writer == null) {
            throw new IllegalArgumentException("The writer in PdfDocument constructor can not be null.");
        }
        documentId = new SequenceId();
        this.writer = writer;
        this.properties = new StampingProperties(properties);
        open(writer.properties.pdfVersion);
    }

    /**
     * Opens PDF document in the stamping mode.
     * <br>
     *
     * @param reader PDF reader.
     * @param writer PDF writer.
     */
    public PdfDocument(PdfReader reader, PdfWriter writer) {
        this(reader, writer, new StampingProperties());
    }

    /**
     * Open PDF document in stamping mode.
     *
     * @param reader     PDF reader.
     * @param writer     PDF writer.
     * @param properties properties of the stamping process
     */
    public PdfDocument(PdfReader reader, PdfWriter writer, StampingProperties properties) {
        if (reader == null) {
            throw new IllegalArgumentException("The reader in PdfDocument constructor can not be null.");
        }
        if (writer == null) {
            throw new IllegalArgumentException("The writer in PdfDocument constructor can not be null.");
        }
        documentId = new SequenceId();
        this.reader = reader;
        this.writer = writer;
        this.properties = properties;

        boolean writerHasEncryption = writerHasEncryption();
        if (properties.appendMode && writerHasEncryption) {
            LOGGER.warn(IoLogMessageConstant.WRITER_ENCRYPTION_IS_IGNORED_APPEND);
        }
        if (properties.preserveEncryption && writerHasEncryption) {
            LOGGER.warn(IoLogMessageConstant.WRITER_ENCRYPTION_IS_IGNORED_PRESERVE);
        }

        open(writer.properties.pdfVersion);
    }

    /**
     * Checks if the document closing has been started or not.
     *
     * @return {@code true} if closing process has been started, otherwise {@code false}
     */
    public boolean isClosing() {
        return isClosing;
    }

    /**
     * Sets the XMP Metadata.
     * <p>
     * The XMP Metadata values are synchronized with information dictionary.
     *
     * @param xmpMeta the xmpMetadata to set
     * @param serializeOptions serialization options
     *
     * @throws XMPException on serialization errors
     */
    public void setXmpMetadata(XMPMeta xmpMeta, SerializeOptions serializeOptions) throws XMPException {
        this.serializeOptions = serializeOptions;
        this.xmpMetadataBytes = XMPMetaFactory.serializeToBuffer(xmpMeta, serializeOptions);
        this.xmpMetadata = xmpMeta;
    }

    /**
     * Sets the XMP Metadata.
     * <p>
     * The XMP Metadata values are synchronized with information dictionary.
     * <p>
     * {@link PdfDocument#serializeOptions} will be used for serialization, they
     * can be changed by {@link PdfDocument#setSerializeOptions(SerializeOptions)}.
     *
     * @param xmpMeta the xmpMetadata to set
     *
     * @throws XMPException on serialization errors
     */
    public void setXmpMetadata(XMPMeta xmpMeta) throws XMPException {
        setXmpMetadata(xmpMeta, serializeOptions);
    }

    /**
     * Sets the XMP Metadata.
     * <p>
     * The XMP Metadata values are synchronized with information dictionary.
     *
     * @param xmpMetadata the xmpMetadata bytes to set
     */
    protected void setXmpMetadata(byte[] xmpMetadata) {
        this.xmpMetadataBytes = xmpMetadata;
        this.xmpMetadata = null;
        try {
            getXmpMetadata();
        } catch (XMPException e) {
            LOGGER.error(IoLogMessageConstant.EXCEPTION_WHILE_UPDATING_XMPMETADATA, e);
        }
    }

    /**
     * Gets XMP Metadata.
     * <p>
     * XMP Metadata is lazy initialized. It will be initialized during the first call of this method.
     * <p>
     * To update XMP Metadata of the document, use {@link PdfDocument#setXmpMetadata(XMPMeta)} method.
     *
     * @return existed XMP Metadata
     *
     * @throws XMPException on serialization errors
     */
    public XMPMeta getXmpMetadata() throws XMPException {
        return getXmpMetadata(false);
    }

    /**
     * Gets XMP Metadata or create a new one.
     * <p>
     * XMP Metadata is lazy initialized. It will be initialized during the first call of this method.
     * <p>
     * To update XMP Metadata of the document, use {@link PdfDocument#setXmpMetadata(XMPMeta)} method.
     *
     * @param createNew if true, create a new empty XMP Metadata if it did not present
     *
     * @return existed or newly created XMP Metadata
     *
     * @throws XMPException on serialization errors
     */
    public XMPMeta getXmpMetadata(boolean createNew) throws XMPException {
        if (xmpMetadata == null) {
            final byte[] bytes = getXmpMetadataBytes(createNew);
            xmpMetadata = bytes == null ? null : XMPMetaFactory.parseFromBuffer(bytes);
        }
        return xmpMetadata;
    }

    /**
     * Gets XMP Metadata.
     * <p>
     * XMP Metadata is lazy initialized. It will be initialized during the first call of this method.
     * <p>
     * To update XMP Metadata of the document, use {@link PdfDocument#setXmpMetadata(XMPMeta)} method.
     *
     * @return existed XMP Metadata bytes
     */
    public byte[] getXmpMetadataBytes() {
        return getXmpMetadataBytes(false);
    }

    /**
     * Gets XMP Metadata or create a new one.
     * <p>
     * XMP Metadata is lazy initialized. It will be initialized during the first call of this method.
     * <p>
     * To update XMP Metadata of the document, use {@link PdfDocument#setXmpMetadata(XMPMeta)} method.
     *
     * @param createNew if true, create a new empty XMP Metadata if it did not present
     *
     * @return existed or newly created XMP Metadata byte array
     */
    public byte[] getXmpMetadataBytes(boolean createNew) {
        checkClosingStatus();
        if (xmpMetadataBytes == null) {
            PdfStream xmpMetadataStream = catalog.getPdfObject().getAsStream(PdfName.Metadata);
            if (xmpMetadataStream != null) {
                xmpMetadataBytes = xmpMetadataStream.getBytes();
            }
        }
        if (createNew && xmpMetadataBytes == null) {
            XMPMeta xmpMeta = XMPMetaFactory.create();
            xmpMeta.setObjectName(XMPConst.TAG_XMPMETA);
            xmpMeta.setObjectName("");
            try {
                xmpMeta.setProperty(XMPConst.NS_DC, PdfConst.Format, "application/pdf");
                setXmpMetadata(xmpMeta);
            } catch (XMPException ignored) {
            }
        }
        if (xmpMetadataBytes == null) {
            return null;
        }
        return Arrays.copyOf(xmpMetadataBytes, xmpMetadataBytes.length);
    }

    /**
     * Gets PdfObject by object number.
     *
     * @param objNum object number.
     *
     * @return {@link PdfObject} or {@code null}, if object not found.
     */
    public PdfObject getPdfObject(int objNum) {
        checkClosingStatus();
        PdfIndirectReference reference = xref.get(objNum);
        if (reference == null) {
            return null;
        } else {
            return reference.getRefersTo();
        }
    }


    /**
     * Get number of indirect objects in the document.
     *
     * @return number of indirect objects.
     */
    public int getNumberOfPdfObjects() {
        return xref.size();
    }

    /**
     * Gets the page by page number.
     *
     * @param pageNum page number.
     *
     * @return page by page number.
     *
     * @throws PdfException in case the page tree is broken
     */
    public PdfPage getPage(int pageNum) {
        checkClosingStatus();
        return catalog.getPageTree().getPage(pageNum);
    }

    /**
     * Gets the {@link PdfPage} instance by {@link PdfDictionary}.
     *
     * @param pageDictionary {@link PdfDictionary} that present page.
     *
     * @return page by {@link PdfDictionary}.
     */
    public PdfPage getPage(PdfDictionary pageDictionary) {
        checkClosingStatus();
        return catalog.getPageTree().getPage(pageDictionary);
    }

    /**
     * Get the first page of the document.
     *
     * @return first page of the document.
     */
    public PdfPage getFirstPage() {
        checkClosingStatus();
        return getPage(1);
    }

    /**
     * Gets the last page of the document.
     *
     * @return last page.
     */
    public PdfPage getLastPage() {
        return getPage(getNumberOfPages());
    }

    /**
     * Gets current memory limits handler
     *
     * @return {@code MemoryLimitsAwareHandler} instance
     */
    public MemoryLimitsAwareHandler getMemoryLimitsAwareHandler() {
        return memoryLimitsAwareHandler;
    }

    /**
     * Marks {@link PdfStream} object as embedded file stream. Note that this method is for internal usage.
     * To add an embedded file to the PDF document please use specialized API for file attachments.
     * (e.g. {@link PdfDocument#addFileAttachment(String, PdfFileSpec)}, {@link PdfPage#addAnnotation(PdfAnnotation)})
     *
     * @param stream to be marked as embedded file stream
     */
    public void markStreamAsEmbeddedFile(PdfStream stream) {
        encryptedEmbeddedStreamsHandler.storeEmbeddedStream(stream);
    }

    /**
     * Creates and adds new page to the end of document.
     *
     * @return added page
     */
    public PdfPage addNewPage() {
        return addNewPage(getDefaultPageSize());
    }

    /**
     * Creates and adds new page with the specified page size.
     *
     * @param pageSize page size of the new page
     *
     * @return added page
     */
    public PdfPage addNewPage(PageSize pageSize) {
        checkClosingStatus();
        PdfPage page = getPageFactory().createPdfPage(this, pageSize);
        checkAndAddPage(page);
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.START_PAGE, page));
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
        return page;
    }

    /**
     * Creates and inserts new page to the document.
     *
     * @param index position to addPage page to
     *
     * @return inserted page
     *
     * @throws PdfException in case {@code page} is flushed
     */
    public PdfPage addNewPage(int index) {
        return addNewPage(index, getDefaultPageSize());
    }

    /**
     * Creates and inserts new page to the document.
     *
     * @param index    position to addPage page to
     * @param pageSize page size of the new page
     *
     * @return inserted page
     *
     * @throws PdfException in case {@code page} is flushed
     */
    public PdfPage addNewPage(int index, PageSize pageSize) {
        checkClosingStatus();
        PdfPage page = getPageFactory().createPdfPage(this, pageSize);
        checkAndAddPage(index, page);
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.START_PAGE, page));
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
        return page;
    }

    /**
     * Adds page to the end of document.
     *
     * @param page page to add.
     *
     * @return added page.
     *
     * @throws PdfException in case {@code page} is flushed
     */
    public PdfPage addPage(PdfPage page) {
        checkClosingStatus();
        checkAndAddPage(page);
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
        return page;
    }

    /**
     * Inserts page to the document.
     *
     * @param index position to addPage page to
     * @param page  page to addPage
     *
     * @return inserted page
     *
     * @throws PdfException in case {@code page} is flushed
     */
    public PdfPage addPage(int index, PdfPage page) {
        checkClosingStatus();
        checkAndAddPage(index, page);
        dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.INSERT_PAGE, page));
        return page;
    }

    /**
     * Gets number of pages of the document.
     *
     * @return number of pages.
     */
    public int getNumberOfPages() {
        checkClosingStatus();
        return catalog.getPageTree().getNumberOfPages();
    }

    /**
     * Gets page number by page.
     *
     * @param page the page.
     *
     * @return page number.
     */
    public int getPageNumber(PdfPage page) {
        checkClosingStatus();
        return catalog.getPageTree().getPageNumber(page);
    }

    /**
     * Gets page number by {@link PdfDictionary}.
     *
     * @param pageDictionary {@link PdfDictionary} that present page.
     *
     * @return page number by {@link PdfDictionary}.
     */
    public int getPageNumber(PdfDictionary pageDictionary) {
        return catalog.getPageTree().getPageNumber(pageDictionary);
    }

    /**
     * Moves page to new place in same document with all it tag structure
     *
     * @param page         page to be moved in document if present
     * @param insertBefore indicates before which page new one will be inserted to
     *
     * @return <tt>true</tt> if this document contained the specified page
     */
    public boolean movePage(PdfPage page, int insertBefore) {
        checkClosingStatus();
        int pageNum = getPageNumber(page);
        if (pageNum > 0) {
            movePage(pageNum, insertBefore);
            return true;
        }
        return false;
    }

    /**
     * Moves page to new place in same document with all it tag structure
     *
     * @param pageNumber   number of Page that will be moved
     * @param insertBefore indicates before which page new one will be inserted to
     */
    public void movePage(int pageNumber, int insertBefore) {
        checkClosingStatus();
        if (insertBefore < 1 || insertBefore > getNumberOfPages() + 1) {
            throw new IndexOutOfBoundsException(
                    MessageFormatUtil.format(KernelExceptionMessageConstant.REQUESTED_PAGE_NUMBER_IS_OUT_OF_BOUNDS,
                            insertBefore));
        }
        PdfPage page = getPage(pageNumber);
        if (isTagged()) {
            getStructTreeRoot().move(page, insertBefore);
            getTagStructureContext().normalizeDocumentRootTag();
        }
        PdfPage removedPage = catalog.getPageTree().removePage(pageNumber);
        if (insertBefore > pageNumber) {
            --insertBefore;
        }
        catalog.getPageTree().addPage(insertBefore, removedPage);
    }

    /**
     * Removes the first occurrence of the specified page from this document,
     * if it is present. Returns <tt>true</tt> if this document
     * contained the specified element (or equivalently, if this document
     * changed as a result of the call).
     *
     * @param page page to be removed from this document, if present
     *
     * @return <tt>true</tt> if this document contained the specified page
     */
    public boolean removePage(PdfPage page) {
        checkClosingStatus();
        int pageNum = getPageNumber(page);
        if (pageNum >= 1) {
            removePage(pageNum);
            return true;
        }
        return false;
    }

    /**
     * Removes page from the document by page number.
     *
     * @param pageNum the one-based index of the PdfPage to be removed
     */
    public void removePage(int pageNum) {
        checkClosingStatus();

        PdfPage removedPage = getPage(pageNum);
        if (removedPage != null && removedPage.isFlushed() && (isTagged() || hasAcroForm())) {
            throw new PdfException(KernelExceptionMessageConstant.FLUSHED_PAGE_CANNOT_BE_REMOVED);
        }

        if (removedPage != null) {
            catalog.removeOutlines(removedPage);
            removeUnusedWidgetsFromFields(removedPage);
            if (isTagged()) {
                getTagStructureContext().removePageTags(removedPage);
            }
            if (!removedPage.isFlushed()) {
                removedPage.getPdfObject().remove(PdfName.Parent);
                removedPage.getPdfObject().getIndirectReference().setFree();
            }

            dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.REMOVE_PAGE, removedPage));
        }

        catalog.getPageTree().removePage(pageNum);
    }


    /**
     * Gets the container containing all available dependencies.
     *
     * @return the container containing all available dependencies.
     */
    public DIContainer getDiContainer() {
        return diContainer;
    }

    /**
     * Gets document information dictionary.
     * <p>
     * {@link PdfDocument#info} is lazy initialized. It will be initialized during the first call of this method.
     * <p>
     * The information dictionary values are synchronized with document XMP Metadata.
     *
     * @return document information dictionary.
     */
    public PdfDocumentInfo getDocumentInfo() {
        checkClosingStatus();
        if (info == null) {
            PdfDictionary infoDict = trailer == null ? null : trailer.getAsDictionary(PdfName.Info);
            info = new PdfDocumentInfo(infoDict == null ? new PdfDictionary() : infoDict, this);
            try {
                XmpMetaInfoConverter.appendMetadataToInfo(getXmpMetadata(), info);
            } catch (XMPException ignored) {
            }
        }
        return info;
    }

    /**
     * Gets original document id
     * <p>
     * In order to set originalDocumentId  {@link WriterProperties#setInitialDocumentId} should be used
     *
     * @return original document id
     */
    public PdfString getOriginalDocumentId() {
        return originalDocumentId;
    }


    /**
     * Gets modified document id
     * <p>
     * In order to set modifiedDocumentId {@link WriterProperties#setModifiedDocumentId} should be used
     *
     * @return modified document id
     */
    public PdfString getModifiedDocumentId() {
        return modifiedDocumentId;
    }

    /**
     * Gets default page size.
     * New pages by default are created with this size.
     *
     * @return default page size
     */
    public PageSize getDefaultPageSize() {
        return defaultPageSize;
    }

    /**
     * Sets default page size.
     * New pages by default will be created with this size.
     *
     * @param pageSize page size to be set as default
     */
    public void setDefaultPageSize(PageSize pageSize) {
        defaultPageSize = pageSize;
    }

    /**
     * Adds new event handler.
     *
     * @param type a type of event to be handled
     * @param handler event handler
     */
    public void addEventHandler(String type, AbstractPdfDocumentEventHandler handler) {
        handler.addType(type);
        documentHandlers.add(handler);
    }

    /**
     * Dispatches an event.
     *
     * @param event the {@link AbstractPdfDocumentEvent} to be dispatched
     */
    public void dispatchEvent(AbstractPdfDocumentEvent event) {
        event.setDocument(this);
        for (final IEventHandler handler : documentHandlers) {
            handler.onEvent(event);
        }
    }

    /**
     * Checks if provided event handler assigned for this document.
     *
     * @param handler the {@link AbstractPdfDocumentEventHandler} to check
     *
     * @return {@code true} if event handler is assigned for this document, {@code false} otherwise
     */
    public boolean hasEventHandler(AbstractPdfDocumentEventHandler handler) {
        return documentHandlers.contains(handler);
    }

    /**
     * Removes event handler.
     *
     * @param handler {@link AbstractPdfDocumentEventHandler} event handler to remove for this document
     */
    public void removeEventHandler(AbstractPdfDocumentEventHandler handler) {
        documentHandlers.remove(handler);
    }

    /**
     * Removes all event handlers for this document.
     */
    public void removeAllHandlers() {
        documentHandlers.clear();
    }

    /**
     * Gets {@code PdfWriter} associated with the document.
     *
     * @return PdfWriter associated with the document.
     */
    public PdfWriter getWriter() {
        checkClosingStatus();
        return writer;
    }

    /**
     * Gets {@code PdfReader} associated with the document.
     *
     * @return PdfReader associated with the document.
     */
    public PdfReader getReader() {
        checkClosingStatus();
        return reader;
    }

    /**
     * Returns {@code true} if the document is opened in append mode, and {@code false} otherwise.
     *
     * @return {@code true} if the document is opened in append mode, and {@code false} otherwise.
     */
    public boolean isAppendMode() {
        checkClosingStatus();
        return properties.appendMode;
    }

    /**
     * Creates next available indirect reference.
     *
     * @return created indirect reference.
     */
    public PdfIndirectReference createNextIndirectReference() {
        checkClosingStatus();
        return xref.createNextIndirectReference(this);
    }

    /**
     * Gets PDF version.
     *
     * @return PDF version.
     */
    public PdfVersion getPdfVersion() {
        return pdfVersion;
    }

    /**
     * Gets PDF catalog.
     *
     * @return PDF catalog.
     */
    public PdfCatalog getCatalog() {
        checkClosingStatus();
        return catalog;
    }

    /**
     * Close PDF document.
     */
    @Override
    public void close() {
        if (closed) {
            return;
        }
        isClosing = true;
        try {
            if (writer != null) {
                if (catalog.isFlushed()) {
                    throw new PdfException(
                            KernelExceptionMessageConstant.CANNOT_CLOSE_DOCUMENT_WITH_ALREADY_FLUSHED_PDF_CATALOG);
                }

                EventManager manager = EventManager.getInstance();
                manager.onEvent(new NumberOfPagesStatisticsEvent(catalog.getPageTree().getNumberOfPages(),
                        ITextCoreProductData.getInstance()));
                // The event will prepare document for flushing, i.e. will set an appropriate producer line
                manager.onEvent(new FlushPdfDocumentEvent(this));
                dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.START_DOCUMENT_CLOSING));

                updateXmpMetadata();
                // In PDF 2.0, all the values except CreationDate and ModDate are deprecated. Remove them now
                if (pdfVersion.compareTo(PdfVersion.PDF_2_0) >= 0) {
                    for (PdfName deprecatedKey : PdfDocumentInfo.PDF20_DEPRECATED_KEYS) {
                        getDocumentInfo().getPdfObject().remove(deprecatedKey);
                    }
                }
                if (getXmpMetadataBytes() != null) {
                    PdfStream xmp = catalog.getPdfObject().getAsStream(PdfName.Metadata);
                    if (isAppendMode() && xmp != null && !xmp.isFlushed() && xmp.getIndirectReference() != null) {
                        // Use existing object for append mode
                        xmp.setData(getXmpMetadataBytes());
                        xmp.setModified();
                    } else {
                        // Create new object
                        xmp = (PdfStream) new PdfStream().makeIndirect(this);
                        xmp.getOutputStream().write(getXmpMetadataBytes());
                        catalog.getPdfObject().put(PdfName.Metadata, xmp);
                        catalog.setModified();
                    }
                    xmp.put(PdfName.Type, PdfName.Metadata);
                    xmp.put(PdfName.Subtype, PdfName.XML);
                    if (writer.crypto != null && !writer.crypto.isMetadataEncrypted()) {
                        PdfArray ar = new PdfArray();
                        ar.add(PdfName.Crypt);
                        xmp.put(PdfName.Filter, ar);
                    }
                }

                if (!properties.appendMode && catalog.isOCPropertiesMayHaveChanged()) {
                    catalog.getPdfObject().put(PdfName.OCProperties, catalog.getOCProperties(false).getPdfObject());
                }

                checkIsoConformance(new PdfDocumentValidationContext(this, getDocumentFonts()));

                if (getNumberOfPages() == 0) {
                    // Add new page here, not in PdfPagesTree#generateTree method, so that any page
                    // operations are available when handling the START_PAGE and INSERT_PAGE events
                    addNewPage();
                }

                PdfObject crypto = null;
                final Set<PdfIndirectReference> forbiddenToFlush = new HashSet<>();
                documentInfoHelper.adjustDocumentInfo(getDocumentInfo());
                // The following 2 operators prevent the possible inconsistency between root and info
                // entries existing in the trailer object and corresponding fields. This inconsistency
                // may appear when user gets trailer and explicitly sets new root or info dictionaries.
                if (documentInfoHelper.shouldAddDocumentInfoToTrailer()) {
                    trailer.put(PdfName.Info, getDocumentInfo().getPdfObject());
                }
                trailer.put(PdfName.Root, catalog.getPdfObject());
                if (properties.appendMode) {
                    if (structTreeRoot != null) {
                        tryFlushTagStructure(true);
                    }
                    if (catalog.isOCPropertiesMayHaveChanged() && catalog.getOCProperties(false).getPdfObject()
                            .isModified()) {
                        catalog.getOCProperties(false).flush();
                    }
                    if (catalog.pageLabels != null) {
                        catalog.put(PdfName.PageLabels, catalog.pageLabels.buildTree());
                    }

                    for (Map.Entry<PdfName, PdfNameTree> entry : catalog.nameTrees.entrySet()) {
                        PdfNameTree tree = entry.getValue();
                        if (tree.isModified()) {
                            ensureTreeRootAddedToNames(tree.buildTree().makeIndirect(this), entry.getKey());
                        }
                    }

                    PdfObject pageRoot = catalog.getPageTree().generateTree();
                    if (catalog.getPdfObject().isModified() || pageRoot.isModified()) {
                        catalog.put(PdfName.Pages, pageRoot);
                        catalog.getPdfObject().flush(false);
                    }
                    if (getDocumentInfo().getPdfObject().isModified()) {
                        getDocumentInfo().getPdfObject().flush(false);
                    }
                    flushFonts();

                    if (writer.crypto != null) {
                        assert reader.decrypt.getPdfObject()
                                == writer.crypto.getPdfObject() : "Conflict with source encryption";
                        crypto = reader.decrypt.getPdfObject();
                        if (crypto.getIndirectReference() != null) {
                            // Checking just for extra safety, encryption dictionary shall never be direct.
                            forbiddenToFlush.add(crypto.getIndirectReference());
                        }
                    }

                    writer.flushModifiedWaitingObjects(forbiddenToFlush);
                    for (int i = 0; i < xref.size(); i++) {
                        PdfIndirectReference indirectReference = xref.get(i);
                        if (indirectReference != null && !indirectReference.isFree() && indirectReference.checkState(
                                PdfObject.MODIFIED) && !indirectReference.checkState(PdfObject.FLUSHED)
                                && !forbiddenToFlush.contains(indirectReference)) {
                            indirectReference.setFree();
                        }
                    }
                } else {
                    if (catalog.isOCPropertiesMayHaveChanged()) {
                        catalog.getOCProperties(false).flush();
                    }
                    if (catalog.pageLabels != null) {
                        catalog.put(PdfName.PageLabels, catalog.pageLabels.buildTree());
                    }

                    catalog.getPdfObject().put(PdfName.Pages, catalog.getPageTree().generateTree());

                    for (Map.Entry<PdfName, PdfNameTree> entry : catalog.nameTrees.entrySet()) {
                        PdfNameTree tree = entry.getValue();
                        if (tree.isModified()) {
                            ensureTreeRootAddedToNames(tree.buildTree().makeIndirect(this), entry.getKey());
                        }
                    }

                    for (int pageNum = 1; pageNum <= getNumberOfPages(); pageNum++) {
                        PdfPage page = getPage(pageNum);
                        if (page != null) {
                            page.flush();
                        }
                    }
                    if (structTreeRoot != null) {
                        tryFlushTagStructure(false);
                    }
                    catalog.getPdfObject().flush(false);
                    getDocumentInfo().getPdfObject().flush(false);
                    flushFonts();

                    if (writer.crypto != null) {
                        crypto = writer.crypto.getPdfObject();
                        crypto.makeIndirect(this);
                        forbiddenToFlush.add(crypto.getIndirectReference());
                    }

                    writer.flushWaitingObjects(forbiddenToFlush);
                    for (int i = 0; i < xref.size(); i++) {
                        PdfIndirectReference indirectReference = xref.get(i);
                        if (indirectReference != null && !indirectReference.isFree() && !indirectReference.checkState(
                                PdfObject.FLUSHED) && !forbiddenToFlush.contains(indirectReference)) {
                            PdfObject object;
                            if (isFlushUnusedObjects() && !indirectReference.checkState(
                                    PdfObject.ORIGINAL_OBJECT_STREAM)
                                    && (object = indirectReference.getRefersTo(false)) != null) {
                                object.flush();
                            } else {
                                indirectReference.setFree();
                            }
                        }
                    }
                }

                // To avoid encryption of XrefStream and Encryption dictionary remove crypto.
                // NOTE. No need in reverting, because it is the last operation with the document.
                writer.crypto = null;
                checkIsoConformance(new CryptoValidationContext(crypto));

                if (!properties.appendMode && crypto != null) {
                    // no need to flush crypto in append mode, it shall not have changed in this case
                    crypto.flush(false);
                }

                //By this time original and modified document ids should always be not null due to initializing in
                // either writer properties, or in the writer init section on document open or from pdfreader. So we
                // shouldn't worry about it being null next
                PdfObject fileId = PdfEncryption.createInfoId(ByteUtils.getIsoBytes(originalDocumentId.getValue()),
                        ByteUtils.getIsoBytes(modifiedDocumentId.getValue()), this.properties.preserveEncryption);
                xref.writeXrefTableAndTrailer(this, fileId, crypto);
                writer.flush();
                if (writer.getOutputStream() instanceof CountOutputStream) {
                    final long amountOfBytes = ((CountOutputStream) writer.getOutputStream()).getAmountOfWrittenBytes();
                    manager.onEvent(new SizeOfPdfStatisticsEvent(amountOfBytes, ITextCoreProductData.getInstance()));
                } else if (writer.getOutputStream() instanceof ByteArrayOutputStream) {
                    final long amountOfBytes = ((ByteArrayOutputStream) writer.getOutputStream()).size();
                    manager.onEvent(new SizeOfPdfStatisticsEvent(amountOfBytes, ITextCoreProductData.getInstance()));
                }
            }
            catalog.getPageTree().clearPageRefs();
        } catch (IOException e) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_CLOSE_DOCUMENT, e, this);
        } finally {
            if (writer != null && isCloseWriter()) {
                try {
                    writer.finish();
                } catch (Exception e) {
                    LOGGER.error(IoLogMessageConstant.PDF_WRITER_CLOSING_FAILED, e);
                }
            }

            if (reader != null && isCloseReader()) {
                try {
                    reader.close();
                } catch (Exception e) {
                    LOGGER.error(IoLogMessageConstant.PDF_READER_CLOSING_FAILED, e);
                }
            }

        }
        closed = true;
    }

    /**
     * Gets close status of the document.
     *
     * @return true, if the document has already been closed, otherwise false.
     */
    public boolean isClosed() {
        return closed;
    }

    /**
     * Gets tagged status of the document.
     *
     * @return true, if the document has tag structure, otherwise false.
     */
    public boolean isTagged() {
        return structTreeRoot != null;
    }

    /**
     * Specifies that document shall contain tag structure.
     * See ISO 32000-1, section 14.8 "Tagged PDF"
     *
     * @return this {@link PdfDocument} instance
     */
    public PdfDocument setTagged() {
        checkClosingStatus();
        if (structTreeRoot == null) {
            structTreeRoot = new PdfStructTreeRoot(this);
            catalog.getPdfObject().put(PdfName.StructTreeRoot, structTreeRoot.getPdfObject());
            updateValueInMarkInfoDict(PdfName.Marked, PdfBoolean.TRUE);

            structParentIndex = 0;
        }
        return this;
    }

    /**
     * Gets {@link PdfStructTreeRoot} of tagged document.
     *
     * @return {@link PdfStructTreeRoot} in case document is tagged, otherwise it returns null.
     *
     * @see #isTagged()
     * @see #getNextStructParentIndex()
     */
    public PdfStructTreeRoot getStructTreeRoot() {
        return structTreeRoot;
    }

    /**
     * Gets next parent index of tagged document.
     *
     * @return -1 if document is not tagged, or &gt;= 0 if tagged.
     *
     * @see #isTagged()
     * @see #getNextStructParentIndex()
     */
    public int getNextStructParentIndex() {
        return structParentIndex < 0 ? -1 : structParentIndex++;
    }

    /**
     * Gets document {@code TagStructureContext}.
     * The document must be tagged, otherwise an exception will be thrown.
     *
     * @return document {@code TagStructureContext}.
     */
    public TagStructureContext getTagStructureContext() {
        checkClosingStatus();
        if (tagStructureContext == null) {
            if (!isTagged()) {
                throw new PdfException(KernelExceptionMessageConstant.MUST_BE_A_TAGGED_DOCUMENT);
            }

            initTagStructureContext();
        }

        return tagStructureContext;
    }

    /**
     * Copies a range of pages from current document to {@code toDocument}.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pageFrom         start of the range of pages to be copied.
     * @param pageTo           end of the range of pages to be copied.
     * @param toDocument       a document to copy pages to.
     * @param insertBeforePage a position where to insert copied pages.
     *
     * @return list of copied pages
     */
    public List<PdfPage> copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, int insertBeforePage) {
        return copyPagesTo(pageFrom, pageTo, toDocument, insertBeforePage, null);
    }
    
    /**
     * Get the {@link PdfConformance}
     *
     * @return the document conformance
     */
    public PdfConformance getConformance() {
        return pdfConformance;
    }

    /**
     * Copies a range of pages from current document to {@code toDocument}. This range is inclusive, both {@code page}
     * and {@code pageTo} are included in list of copied pages.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pageFrom         1-based start of the range of pages to be copied.
     * @param pageTo           1-based end (inclusive) of the range of pages to be copied. This page is included in list
     *                         of copied pages.
     * @param toDocument       a document to copy pages to.
     * @param insertBeforePage a position where to insert copied pages.
     * @param copier           a copier which bears a special copy logic. May be null.
     *                         It is recommended to use the same instance of {@link IPdfPageExtraCopier}
     *                         for the same output document.
     *
     * @return list of new copied pages
     */
    public List<PdfPage> copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, int insertBeforePage,
            IPdfPageExtraCopier copier) {
        List<Integer> pages = new ArrayList<>();
        for (int i = pageFrom; i <= pageTo; i++) {
            pages.add(i);
        }
        return copyPagesTo(pages, toDocument, insertBeforePage, copier);
    }

    /**
     * Copies a range of pages from current document to {@code toDocument} appending copied pages to the end. This range
     * is inclusive, both {@code page} and {@code pageTo} are included in list of copied pages.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pageFrom   1-based start of the range of pages to be copied.
     * @param pageTo     1-based end (inclusive) of the range of pages to be copied. This page is included in list of
     *                   copied pages.
     * @param toDocument a document to copy pages to.
     *
     * @return list of new copied pages
     */
    public List<PdfPage> copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument) {
        return copyPagesTo(pageFrom, pageTo, toDocument, null);
    }

    /**
     * Copies a range of pages from current document to {@code toDocument} appending copied pages to the end. This range
     * is inclusive, both {@code page} and {@code pageTo} are included in list of copied pages.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pageFrom   1-based start of the range of pages to be copied.
     * @param pageTo     1-based end (inclusive) of the range of pages to be copied. This page is included in list of
     *                   copied pages.
     * @param toDocument a document to copy pages to.
     * @param copier     a copier which bears a special copy logic. May be null.
     *                   It is recommended to use the same instance of {@link IPdfPageExtraCopier}
     *                   for the same output document.
     *
     * @return list of new copied pages.
     */
    public List<PdfPage> copyPagesTo(int pageFrom, int pageTo, PdfDocument toDocument, IPdfPageExtraCopier copier) {
        return copyPagesTo(pageFrom, pageTo, toDocument, toDocument.getNumberOfPages() + 1, copier);
    }

    /**
     * Copies a range of pages from current document to {@code toDocument}.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pagesToCopy      list of pages to be copied.
     * @param toDocument       a document to copy pages to.
     * @param insertBeforePage a position where to insert copied pages.
     *
     * @return list of new copied pages
     */
    public List<PdfPage> copyPagesTo(List<Integer> pagesToCopy, PdfDocument toDocument, int insertBeforePage) {
        return copyPagesTo(pagesToCopy, toDocument, insertBeforePage, null);
    }

    /**
     * Copies a range of pages from current document to {@code toDocument}.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pagesToCopy      list of pages to be copied.
     * @param toDocument       a document to copy pages to.
     * @param insertBeforePage a position where to insert copied pages.
     * @param copier           a copier which bears a special copy logic. May be null.
     *                         It is recommended to use the same instance of {@link IPdfPageExtraCopier}
     *                         for the same output document.
     *
     * @return list of new copied pages
     */
    public List<PdfPage> copyPagesTo(List<Integer> pagesToCopy, PdfDocument toDocument, int insertBeforePage,
            IPdfPageExtraCopier copier) {
        if (pagesToCopy.isEmpty()) {
            return Collections.<PdfPage>emptyList();
        }
        pendingDestinationMutations.clear();
        checkClosingStatus();
        List<PdfPage> copiedPages = new ArrayList<>();
        Map<PdfPage, PdfPage> page2page = new LinkedHashMap<>();
        Set<PdfOutline> outlinesToCopy = new HashSet<>();

        List<Map<PdfPage, PdfPage>> rangesOfPagesWithIncreasingNumbers = new ArrayList<>();
        int lastCopiedPageNum = (int) pagesToCopy.get(0);

        int pageInsertIndex = insertBeforePage;
        boolean insertInBetween = insertBeforePage < toDocument.getNumberOfPages() + 1;

        for (Integer pageNum : pagesToCopy) {
            PdfPage page = getPage((int) pageNum);
            PdfPage newPage = page.copyTo(toDocument, copier, true,
                    insertInBetween ? pageInsertIndex : -1);
            copiedPages.add(newPage);
            page2page.put(page, newPage);

            if (lastCopiedPageNum >= pageNum) {
                rangesOfPagesWithIncreasingNumbers.add(new HashMap<PdfPage, PdfPage>());
            }
            int lastRangeInd = rangesOfPagesWithIncreasingNumbers.size() - 1;
            rangesOfPagesWithIncreasingNumbers.get(lastRangeInd).put(page, newPage);

            pageInsertIndex++;
            if (toDocument.hasOutlines()) {
                List<PdfOutline> pageOutlines = page.getOutlines(false);
                if (pageOutlines != null) {
                    outlinesToCopy.addAll(pageOutlines);
                }
            }
            lastCopiedPageNum = (int) pageNum;
        }

        resolveDestinations(toDocument, page2page);

        // Copying OCGs should go after copying LinkAnnotations
        if (getCatalog() != null && getCatalog().getPdfObject().getAsDictionary(PdfName.OCProperties) != null) {
            OcgPropertiesCopier.copyOCGProperties(this, toDocument, page2page);
            if(toDocument.getCatalog().getPdfObject().getAsDictionary(PdfName.OCProperties) != null){
                toDocument.getCatalog().setOcgCopied(true);
            }
        }

        // It's important to copy tag structure after link annotations were copied, because object content items in tag
        // structure are not copied in case if their's OBJ key is annotation and doesn't contain /P entry.
        if (toDocument.isTagged()) {
            if (isTagged()) {
                try {
                    for (Map<PdfPage, PdfPage> increasingPagesRange : rangesOfPagesWithIncreasingNumbers) {
                        if (insertInBetween) {
                            getStructTreeRoot().copyTo(toDocument, insertBeforePage, increasingPagesRange);
                        } else {
                            getStructTreeRoot().copyTo(toDocument, increasingPagesRange);
                        }
                        insertBeforePage += increasingPagesRange.size();
                    }
                    toDocument.getTagStructureContext().normalizeDocumentRootTag();
                } catch (Exception e) {
                    throw new PdfException(
                            KernelExceptionMessageConstant.
                                    TAG_STRUCTURE_COPYING_FAILED_IT_MIGHT_BE_CORRUPTED_IN_ONE_OF_THE_DOCUMENTS,
                            e);
                }
                if (copier instanceof IPdfPageFormCopier) {
                    ((IPdfPageFormCopier) copier).recreateAcroformToProcessCopiedFields(toDocument);
                }
            } else {
                LOGGER.warn(IoLogMessageConstant.NOT_TAGGED_PAGES_IN_TAGGED_DOCUMENT);
            }
        }
        if (catalog.isOutlineMode()) {
            copyOutlines(outlinesToCopy, toDocument, page2page);
        }
        return copiedPages;
    }

    /**
     * Copies a range of pages from current document to {@code toDocument} appending copied pages to the end.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pagesToCopy list of pages to be copied.
     * @param toDocument  a document to copy pages to.
     *
     * @return list of copied pages
     */
    public List<PdfPage> copyPagesTo(List<Integer> pagesToCopy, PdfDocument toDocument) {
        return copyPagesTo(pagesToCopy, toDocument, null);
    }

    /**
     * Copies a range of pages from current document to {@code toDocument} appending copied pages to the end.
     * Use this method if you want to copy pages across tagged documents.
     * This will keep resultant PDF structure consistent.
     * <p>
     * If outlines destination names are the same in different documents, all
     * such outlines will lead to a single location in the resultant document.
     * In this case iText will log a warning. This can be avoided by renaming
     * destinations names in the source document.
     *
     * @param pagesToCopy list of pages to be copied.
     * @param toDocument  a document to copy pages to.
     * @param copier      a copier which bears a special copy logic. May be null.
     *                    It is recommended to use the same instance of {@link IPdfPageExtraCopier}
     *                    for the same output document.
     *
     * @return list of copied pages
     */
    public List<PdfPage> copyPagesTo(List<Integer> pagesToCopy, PdfDocument toDocument, IPdfPageExtraCopier copier) {
        return copyPagesTo(pagesToCopy, toDocument, toDocument.getNumberOfPages() + 1, copier);
    }

    /**
     * Flush all copied objects and remove them from copied cache.
     * <p>
     * Note, if you will copy objects from the same document, duplicated objects will be created.
     * That's why usually this method is meant to be used when all copying from source document is finished.
     * For other cases one can also consider other flushing mechanisms, e.g. pages-based flushing.
     *
     * @param sourceDoc source document
     */
    public void flushCopiedObjects(PdfDocument sourceDoc) {
        if (getWriter() != null) {
            getWriter().flushCopiedObjects(sourceDoc.getDocumentId());
        }
    }

    /**
     * Checks, whether {@link #close()} method will close associated PdfReader.
     *
     * @return true, {@link #close()} method is going to close associated PdfReader, otherwise false.
     */
    public boolean isCloseReader() {
        return closeReader;
    }

    /**
     * Sets, whether {@link #close()} method shall close associated PdfReader.
     *
     * @param closeReader true, {@link #close()} method shall close associated PdfReader, otherwise false.
     */
    public void setCloseReader(boolean closeReader) {
        checkClosingStatus();
        this.closeReader = closeReader;
    }

    /**
     * Checks, whether {@link #close()} method will close associated PdfWriter.
     *
     * @return true, {@link #close()} method is going to close associated PdfWriter, otherwise false.
     */
    public boolean isCloseWriter() {
        return closeWriter;
    }

    /**
     * Sets, whether {@link #close()} method shall close associated PdfWriter.
     *
     * @param closeWriter true, {@link #close()} method shall close associated PdfWriter, otherwise false.
     */
    public void setCloseWriter(boolean closeWriter) {
        checkClosingStatus();
        this.closeWriter = closeWriter;
    }

    /**
     * Checks, whether {@link #close()} will flush unused objects,
     * e.g. unreachable from PDF Catalog. By default - false.
     *
     * @return false, if {@link #close()} shall not flush unused objects, otherwise true.
     */
    public boolean isFlushUnusedObjects() {
        return flushUnusedObjects;
    }

    /**
     * Sets, whether {@link #close()} shall flush unused objects,
     * e.g. unreachable from PDF Catalog.
     *
     * @param flushUnusedObjects false, if {@link #close()} shall not flush unused objects, otherwise true.
     */
    public void setFlushUnusedObjects(boolean flushUnusedObjects) {
        checkClosingStatus();
        this.flushUnusedObjects = flushUnusedObjects;
    }

    /**
     * This method returns a complete outline tree of the whole document.
     *
     * @param updateOutlines if the flag is {@code true}, the method reads the whole document and creates outline tree.
     *                       If the flag is {@code false}, the method gets cached outline tree
     *                       (if it was cached via calling getOutlines method before).
     *
     * @return fully initialize {@link PdfOutline} object.
     */
    public PdfOutline getOutlines(boolean updateOutlines) {
        checkClosingStatus();
        return catalog.getOutlines(updateOutlines);
    }

    /**
     * This method initializes an outline tree of the document and sets outline mode to true.
     */
    public void initializeOutlines() {
        checkClosingStatus();
        getOutlines(false);
    }

    /**
     * This methods adds new name in the Dests NameTree. It throws an exception, if the name already exists.
     *
     * @param key   Name of the destination.
     * @param value An object destination refers to. Must be an array or a dictionary with key /D and array.
     *              See ISO 32000-1 12.3.2.3 for more info.
     */
    public void addNamedDestination(String key, PdfObject value) {
        addNamedDestination(new PdfString(key), value);
    }

    /**
     * This methods adds new name in the Dests NameTree. It throws an exception, if the name already exists.
     *
     * @param key   Name of the destination.
     * @param value An object destination refers to. Must be an array or a dictionary with key /D and array.
     *              See ISO 32000-1 12.3.2.3 for more info.
     */
    public void addNamedDestination(PdfString key, PdfObject value) {
        checkClosingStatus();
        if (value.isArray() && ((PdfArray) value).get(0).isNumber()) {
            LOGGER.warn(IoLogMessageConstant.INVALID_DESTINATION_TYPE);
        }
        catalog.addNamedDestination(key, value);
    }

    /**
     * Gets static copy of cross reference table.
     *
     * @return a static copy of cross reference table
     */
    public List<PdfIndirectReference> listIndirectReferences() {
        checkClosingStatus();
        List<PdfIndirectReference> indRefs = new ArrayList<>(xref.size());
        for (int i = 0; i < xref.size(); ++i) {
            PdfIndirectReference indref = xref.get(i);
            if (indref != null) {
                indRefs.add(indref);
            }
        }
        return indRefs;
    }

    /**
     * Gets document trailer.
     *
     * @return document trailer.
     */
    public PdfDictionary getTrailer() {
        checkClosingStatus();
        return trailer;
    }

    /**
     * Adds {@link PdfOutputIntent} that shall specify the colour characteristics of output devices
     * on which the document might be rendered.
     *
     * @param outputIntent {@link PdfOutputIntent} to add.
     *
     * @see PdfOutputIntent
     */
    public void addOutputIntent(PdfOutputIntent outputIntent) {
        checkClosingStatus();
        if (outputIntent == null) {
            return;
        }

        PdfArray outputIntents = catalog.getPdfObject().getAsArray(PdfName.OutputIntents);
        if (outputIntents == null) {
            outputIntents = new PdfArray();
            catalog.put(PdfName.OutputIntents, outputIntents);
        }
        outputIntents.add(outputIntent.getPdfObject());
    }

    /**
     * Checks ISO conformance of the passed context against
     * registered {@link ValidationContainer} inside the {@code PdfDocument}.
     *
     * @param validationContext the context to check
     */
    public void checkIsoConformance(IValidationContext validationContext) {
        if (!this.getDiContainer().isRegistered(ValidationContainer.class)) {
            return;
        }
        ValidationContainer container = this.getDiContainer().getInstance(ValidationContainer.class);
        if (container == null) {
            return;
        }
        container.validate(validationContext);
    }

    /**
     * Adds file attachment at document level.
     *
     * @param key name of the destination.
     * @param fs  {@link PdfFileSpec} object.
     */
    public void addFileAttachment(String key, PdfFileSpec fs) {
        checkClosingStatus();
        catalog.addNameToNameTree(new PdfString(key), fs.getPdfObject(), PdfName.EmbeddedFiles);
    }

    /**
     * Adds file associated with PDF document as a whole and identifies the relationship between them.
     * <p>
     * Associated files may be used in Pdf/A-3 and Pdf 2.0 documents.
     * The method is very similar to {@link PdfDocument#addFileAttachment(String, PdfFileSpec)}.
     * However, besides adding file description to Names tree, it adds file to array value of the AF key in the document
     * catalog.
     * <p>
     * For associated files their associated file specification dictionaries shall include the AFRelationship key
     *
     * @param description the file description
     * @param fs          file specification dictionary of associated file
     *
     * @see PdfDocument#addFileAttachment(String, PdfFileSpec)
     */
    public void addAssociatedFile(String description, PdfFileSpec fs) {
        if (null == ((PdfDictionary) fs.getPdfObject()).get(PdfName.AFRelationship)) {
            LOGGER.error(IoLogMessageConstant.ASSOCIATED_FILE_SPEC_SHALL_INCLUDE_AFRELATIONSHIP);
        }

        PdfArray afArray = catalog.getPdfObject().getAsArray(PdfName.AF);
        if (afArray == null) {
            afArray = (PdfArray) new PdfArray().makeIndirect(this);
            catalog.put(PdfName.AF, afArray);
        }
        afArray.add(fs.getPdfObject());

        addFileAttachment(description, fs);
    }

    /**
     * Returns files associated with PDF document.
     *
     * @return associated files array.
     */
    public PdfArray getAssociatedFiles() {
        checkClosingStatus();
        return catalog.getPdfObject().getAsArray(PdfName.AF);
    }

    /**
     * Gets the encrypted payload of this document,
     * or returns {@code null} if this document isn't an unencrypted wrapper document.
     *
     * @return encrypted payload of this document.
     */
    public PdfEncryptedPayloadDocument getEncryptedPayloadDocument() {
        if (getReader() != null && getReader().isEncrypted()) {
            return null;
        }
        PdfCollection collection = getCatalog().getCollection();
        if (collection != null && collection.isViewHidden()) {
            PdfString documentName = collection.getInitialDocument();
            PdfNameTree embeddedFiles = getCatalog().getNameTree(PdfName.EmbeddedFiles);
            PdfObject fileSpecObject = embeddedFiles.getNames().get(documentName);
            if (fileSpecObject != null && fileSpecObject.isDictionary()) {
                try {
                    PdfFileSpec fileSpec = PdfEncryptedPayloadFileSpecFactory.wrap((PdfDictionary) fileSpecObject);
                    if (fileSpec != null) {
                        final PdfDictionary embeddedDictionary = ((PdfDictionary) fileSpec.getPdfObject()).
                                getAsDictionary(
                                        PdfName.EF);
                        PdfStream stream = embeddedDictionary.getAsStream(PdfName.UF);
                        if (stream == null) {
                            stream = embeddedDictionary.getAsStream(PdfName.F);
                        }
                        if (stream != null) {
                            String documentNameUnicode = documentName.toUnicodeString();
                            return new PdfEncryptedPayloadDocument(stream, fileSpec, documentNameUnicode);
                        }
                    }
                } catch (PdfException e) {
                    LOGGER.error(e.getMessage());
                }
            }
        }
        return null;
    }

    /**
     * Sets an encrypted payload, making this document an unencrypted wrapper document.
     * The file spec shall include the AFRelationship key with a value of EncryptedPayload,
     * and shall include an encrypted payload dictionary.
     *
     * @param fs encrypted payload file spec. {@link PdfEncryptedPayloadFileSpecFactory} can produce one.
     */
    public void setEncryptedPayload(PdfFileSpec fs) {
        if (getWriter() == null) {
            throw new PdfException(
                    KernelExceptionMessageConstant.CANNOT_SET_ENCRYPTED_PAYLOAD_TO_DOCUMENT_OPENED_IN_READING_MODE);
        }
        if (writerHasEncryption()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_SET_ENCRYPTED_PAYLOAD_TO_ENCRYPTED_DOCUMENT);
        }
        if (!PdfName.EncryptedPayload.equals(((PdfDictionary) fs.getPdfObject()).get(PdfName.AFRelationship))) {
            LOGGER.error(IoLogMessageConstant.ENCRYPTED_PAYLOAD_FILE_SPEC_SHALL_HAVE_AFRELATIONSHIP_FILED_EQUAL_TO_ENCRYPTED_PAYLOAD);
        }
        PdfEncryptedPayload encryptedPayload = PdfEncryptedPayload.extractFrom(fs);
        if (encryptedPayload == null) {
            throw new PdfException(
                    KernelExceptionMessageConstant.ENCRYPTED_PAYLOAD_FILE_SPEC_DOES_NOT_HAVE_ENCRYPTED_PAYLOAD_DICTIONARY);
        }
        PdfCollection collection = getCatalog().getCollection();
        if (collection != null) {
            LOGGER.warn(IoLogMessageConstant.COLLECTION_DICTIONARY_ALREADY_EXISTS_IT_WILL_BE_MODIFIED);
        } else {
            collection = new PdfCollection();
            getCatalog().setCollection(collection);
        }
        collection.setView(PdfCollection.HIDDEN);
        String displayName = PdfEncryptedPayloadFileSpecFactory.generateFileDisplay(encryptedPayload);
        collection.setInitialDocument(displayName);
        addAssociatedFile(displayName, fs);
    }

    /**
     * This method retrieves the page labels from a document as an array of String objects.
     *
     * @return {@link String} list of page labels if they were found, or {@code null} otherwise
     */
    public String[] getPageLabels() {
        if (catalog.getPageLabelsTree(false) == null) {
            return null;
        }
        Map<Integer, PdfObject> pageLabels = catalog.getPageLabelsTree(false).getNumbers();
        if (pageLabels.size() == 0) {
            return null;
        }
        String[] labelStrings = new String[getNumberOfPages()];
        int pageCount = 1;
        String prefix = "";
        String type = "D";
        for (int i = 0; i < getNumberOfPages(); i++) {
            if (pageLabels.containsKey(i)) {
                PdfDictionary labelDictionary = (PdfDictionary) pageLabels.get(i);
                PdfNumber pageRange = labelDictionary.getAsNumber(PdfName.St);
                if (pageRange != null) {
                    pageCount = pageRange.intValue();
                } else {
                    pageCount = 1;
                }
                PdfString p = labelDictionary.getAsString(PdfName.P);
                if (p != null) {
                    prefix = p.toUnicodeString();
                } else {
                    prefix = "";
                }
                PdfName t = labelDictionary.getAsName(PdfName.S);
                if (t != null) {
                    type = t.getValue();
                } else {
                    type = "e";
                }
            }
            switch (type) {
                case "R":
                    labelStrings[i] = prefix + RomanNumbering.toRomanUpperCase(pageCount);
                    break;
                case "r":
                    labelStrings[i] = prefix + RomanNumbering.toRomanLowerCase(pageCount);
                    break;
                case "A":
                    labelStrings[i] = prefix + EnglishAlphabetNumbering.toLatinAlphabetNumberUpperCase(pageCount);
                    break;
                case "a":
                    labelStrings[i] = prefix + EnglishAlphabetNumbering.toLatinAlphabetNumberLowerCase(pageCount);
                    break;
                case "e":
                    labelStrings[i] = prefix;
                    break;
                default:
                    labelStrings[i] = prefix + pageCount;
                    break;
            }
            pageCount++;
        }
        return labelStrings;
    }

    /**
     * Indicates if the document has any outlines
     *
     * @return {@code true}, if there are outlines and {@code false} otherwise.
     */
    public boolean hasOutlines() {
        return catalog.hasOutlines();
    }

    /**
     * Sets the flag indicating the presence of structure elements that contain user properties attributes.
     *
     * @param userProperties the user properties flag
     */
    public void setUserProperties(boolean userProperties) {
        PdfBoolean userPropsVal = userProperties ? PdfBoolean.TRUE : PdfBoolean.FALSE;
        updateValueInMarkInfoDict(PdfName.UserProperties, userPropsVal);
    }

    /**
     * Create a new instance of {@link PdfFont} or load already created one.
     *
     * @param dictionary {@link PdfDictionary} that presents {@link PdfFont}.
     *
     * @return instance of {@link PdfFont}
     * <p>
     * Note, PdfFont which created with {@link PdfFontFactory#createFont(PdfDictionary)} won't be cached
     * until it will be added to {@link com.itextpdf.kernel.pdf.canvas.PdfCanvas} or {@link PdfResources}.
     */
    public PdfFont getFont(PdfDictionary dictionary) {
        PdfIndirectReference indirectReference = dictionary.getIndirectReference();
        if (indirectReference != null && documentFonts.containsKey(indirectReference)) {
            return documentFonts.get(indirectReference);
        } else {
            return addFont(PdfFontFactory.createFont(dictionary));
        }
    }

    /**
     * Gets default font for the document: Helvetica, WinAnsi.
     * One instance per document.
     *
     * @return instance of {@link PdfFont} or {@code null} on error.
     */
    public PdfFont getDefaultFont() {
        return defaultFontStrategy.getFont();
    }

    /**
     * Adds a {@link PdfFont} instance to this document so that this font is flushed automatically
     * on document close. As a side effect, the underlying font dictionary is made indirect if it wasn't the case yet
     *
     * @param font a {@link PdfFont} instance to add
     *
     * @return the same PdfFont instance.
     */
    public PdfFont addFont(PdfFont font) {
        font.makeIndirect(this);
        // forbid release for font dictionaries that are stored in #documentFonts collection
        font.setForbidRelease();
        documentFonts.put(font.getPdfObject().getIndirectReference(), font);
        return font;
    }

    /**
     * Registers a product for debugging purposes.
     *
     * @param productData product to be registered.
     *
     * @return true if the product hadn't been registered before.
     */
    public boolean registerProduct(final ProductData productData) {
        return this.fingerPrint.registerProduct(productData);
    }

    /**
     * Returns the object containing the registered products.
     *
     * @return fingerprint object
     */
    public FingerPrint getFingerPrint() {
        return fingerPrint;
    }

    /**
     * Find {@link PdfFont} from loaded fonts with corresponding fontProgram and encoding or CMAP.
     *
     * @param fontProgram a font name or path to a font program
     * @param encoding    an encoding or CMAP
     *
     * @return the font instance, or null if font wasn't found
     */
    public PdfFont findFont(String fontProgram, String encoding) {
        for (PdfFont font : documentFonts.values()) {
            if (!font.isFlushed() && font.isBuiltWith(fontProgram, encoding)) {
                return font;
            }
        }
        return null;
    }

    /**
     * Obtains numeric document id.
     *
     * @return document id
     */
    public long getDocumentId() {
        return documentId.getId();
    }

    /**
     * Obtains document id as a {@link SequenceId}.
     *
     * @return document id
     */
    public SequenceId getDocumentIdWrapper() {
        return documentId;
    }

    /**
     * Gets a persistent XMP metadata serialization options.
     *
     * @return serialize options
     */
    public SerializeOptions getSerializeOptions() {
        return this.serializeOptions;
    }

    /**
     * Sets a persistent XMP metadata serialization options.
     *
     * @param serializeOptions serialize options
     */
    public void setSerializeOptions(SerializeOptions serializeOptions) {
        this.serializeOptions = serializeOptions;
    }

    /**
     * Initialize {@link TagStructureContext}.
     */
    protected void initTagStructureContext() {
        tagStructureContext = new TagStructureContext(this);
    }

    /**
     * Save destinations in a temporary storage for further copying.
     *
     * @param destination        the {@link PdfDestination} to be updated itself.
     * @param onPageAvailable    a destination consumer that will handle the copying when the
     *                           destination still resolves, it gets the new destination as input
     * @param onPageNotAvailable a destination consumer that will handle the copying when the
     *                           destination is not available, it gets the original destination
     *                           as input
     */
    protected void storeDestinationToReaddress(PdfDestination destination,
            Consumer<PdfDestination> onPageAvailable, Consumer<PdfDestination> onPageNotAvailable) {
        pendingDestinationMutations.add(new DestinationMutationInfo(destination, onPageAvailable, onPageNotAvailable));
    }

    /**
     * Flush an object.
     *
     * @param pdfObject     object to flush.
     * @param canBeInObjStm indicates whether object can be placed into object stream.
     *
     * @throws IOException on error.
     */
    protected void flushObject(PdfObject pdfObject, boolean canBeInObjStm) throws IOException {
        boolean flushAllowed = true;
        if (!isClosing && this.getDiContainer().isRegistered(ValidationContainer.class)) {
            ValidationContainer container = this.getDiContainer().getInstance(ValidationContainer.class);
            if (container != null) {
                flushAllowed = container.isPdfObjectChecked(pdfObject);
            }
        }
        if (isClosing || flushAllowed) {
            writer.flushObject(pdfObject, canBeInObjStm);
        } else if (pdfObject.getIndirectReference() != null)  {
            pdfObject.getIndirectReference().setState(PdfObject.MUST_BE_FLUSHED);
        }
    }

    /**
     * Initializes document.
     *
     * @param newPdfVersion new pdf version of the resultant file if stamper is used and the version needs to be
     *                      changed,
     *                      or {@code null} otherwise
     */
    protected void open(PdfVersion newPdfVersion) {
        if (properties != null){
            for (Class<?> aClass : properties.dependencies.keySet()) {
                diContainer.register(aClass, properties.dependencies.get(aClass));
            }
        }
        this.fingerPrint = new FingerPrint();
        this.encryptedEmbeddedStreamsHandler = new EncryptedEmbeddedStreamsHandler(this);

        try {
            final ITextCoreProductEvent event = ITextCoreProductEvent.createProcessPdfEvent(this.getDocumentIdWrapper(),
                    properties.metaInfo,
                    writer == null ? EventConfirmationType.ON_DEMAND : EventConfirmationType.ON_CLOSE);
            EventManager.getInstance().onEvent(event);
            boolean embeddedStreamsSavedOnReading = false;
            if (reader != null) {
                if (reader.pdfDocument != null) {
                    throw new PdfException(KernelExceptionMessageConstant.PDF_READER_HAS_BEEN_ALREADY_UTILIZED);
                }
                reader.pdfDocument = this;
                memoryLimitsAwareHandler = reader.properties.memoryLimitsAwareHandler;
                if (null == memoryLimitsAwareHandler) {
                    memoryLimitsAwareHandler = new MemoryLimitsAwareHandler(reader.tokens.getSafeFile().length());
                }
                xref.setMemoryLimitsAwareHandler(memoryLimitsAwareHandler);
                reader.readPdf();
                if (reader.decrypt != null && reader.decrypt.isEmbeddedFilesOnly()) {
                    encryptedEmbeddedStreamsHandler.storeAllEmbeddedStreams();
                    embeddedStreamsSavedOnReading = true;
                }
                pdfVersion = reader.headerPdfVersion;
                trailer = new PdfDictionary(reader.trailer);

                readDocumentIds();

                PdfDictionary catalogDictionary = (PdfDictionary) trailer.get(PdfName.Root, true);
                if (null == catalogDictionary) {
                    throw new PdfException(KernelExceptionMessageConstant.CORRUPTED_ROOT_ENTRY_IN_TRAILER);
                }
                catalog = new PdfCatalog(catalogDictionary);
                updatePdfVersionFromCatalog();

                PdfDictionary str = catalog.getPdfObject().getAsDictionary(PdfName.StructTreeRoot);
                if (str != null) {
                    tryInitTagStructure(str);
                }
                if (properties.appendMode && (reader.hasRebuiltXref() || reader.hasFixedXref())) {
                    throw new PdfException(
                            KernelExceptionMessageConstant.APPEND_MODE_REQUIRES_A_DOCUMENT_WITHOUT_ERRORS_EVEN_IF_RECOVERY_IS_POSSIBLE);
                }
                pdfConformance = reader.getPdfConformance();
            }
            xref.initFreeReferencesList(this);
            if (writer != null) {
                if (writer.properties.addPdfAXmpMetadata != null || writer.properties.addPdfUaXmpMetadata != null) {
                    pdfConformance = new PdfConformance(writer.properties.addPdfAXmpMetadata,
                            writer.properties.addPdfUaXmpMetadata);
                }
                enableByteArrayWritingMode();
                if (reader != null && reader.hasXrefStm() && writer.properties.isFullCompression == null) {
                    writer.properties.isFullCompression = Boolean.TRUE;
                }
                if (reader != null && !reader.isOpenedWithFullPermission()) {
                    throw new BadPasswordException(BadPasswordException.PdfReaderNotOpenedWithOwnerPassword);
                }
                if (reader != null && properties.preserveEncryption) {
                    writer.crypto = reader.decrypt;
                }
                writer.document = this;
                if (reader == null) {
                    catalog = new PdfCatalog(this);
                    // initialize document info
                    getDocumentInfo().addCreationDate();
                }
                getDocumentInfo().addModDate();

                if (trailer == null ) {
                    trailer = new PdfDictionary();
                }
                // We keep the original trailer of the document to preserve the original document keys,
                // but we have to remove all standard keys that can occur in the trailer to avoid invalid pdfs
                if (!trailer.isEmpty()) {
                    for (final PdfName key : PdfDocument.PDF_NAMES_TO_REMOVE_FROM_ORIGINAL_TRAILER) {
                        trailer.remove(key);
                    }
                }

                trailer.put(PdfName.Root, catalog.getPdfObject().getIndirectReference());

                if (reader != null) {
                    // If the reader's trailer contains an ID entry, let's copy it over to the new trailer
                    if (reader.trailer.containsKey(PdfName.ID)) {
                        trailer.put(PdfName.ID, reader.trailer.get(PdfName.ID));
                    }
                }

                if (writer.properties != null) {
                    PdfString readerModifiedId = modifiedDocumentId;
                    if (writer.properties.initialDocumentId != null && !(reader != null && reader.decrypt != null && (
                            properties.appendMode || properties.preserveEncryption))) {
                        originalDocumentId = writer.properties.initialDocumentId;
                    }
                    if (writer.properties.modifiedDocumentId != null) {
                        modifiedDocumentId = writer.properties.modifiedDocumentId;
                    }
                    if (originalDocumentId == null && modifiedDocumentId != null) {
                        originalDocumentId = modifiedDocumentId;
                    }
                    if (modifiedDocumentId == null) {
                        if (originalDocumentId == null) {
                            originalDocumentId = new PdfString(PdfEncryption.generateNewDocumentId());
                        }
                        modifiedDocumentId = originalDocumentId;
                    }
                    if (writer.properties.modifiedDocumentId == null && modifiedDocumentId.equals(readerModifiedId)) {
                        modifiedDocumentId = new PdfString(PdfEncryption.generateNewDocumentId());
                    }
                }

                assert originalDocumentId != null;
                assert modifiedDocumentId != null;
            }
            if (properties.appendMode) {
                // Due to constructor reader and writer not null.
                assert reader != null;
                RandomAccessFileOrArray file = reader.tokens.getSafeFile();
                int n;
                byte[] buffer = new byte[8192];
                while ((n = file.read(buffer)) > 0) {
                    writer.write(buffer, 0, n);
                }
                file.close();
                writer.write((byte) '\n');

                overrideFullCompressionInWriterProperties(writer.properties, reader.hasXrefStm());

                writer.crypto = reader.decrypt;
                if (writer.crypto != null) {
                    writer.crypto.checkEncryptionRequirements(this);
                    writer.crypto.configureEncryptionParametersFromWriter(this);
                }

                if (newPdfVersion != null) {
                    // In PDF 1.4, a PDF version can also be specified in the Version entry of the document catalog,
                    // essentially updating the version associated with the file by overriding the one specified in
                    // the file header
                    if (pdfVersion.compareTo(PdfVersion.PDF_1_4) >= 0) {
                        // If the header specifies a later version, or if this entry is absent, the document conforms
                        // to the
                        // version specified in the header.

                        // So only update the version if it is older than the one in the header
                        if (newPdfVersion.compareTo(reader.headerPdfVersion) > 0) {
                            catalog.put(PdfName.Version, newPdfVersion.toPdfName());
                            catalog.setModified();
                            pdfVersion = newPdfVersion;
                        }
                    } else {
                        // Formally we cannot update version in the catalog as it is not supported for the
                        // PDF version of the original document
                    }
                }
            } else if (writer != null) {
                if (newPdfVersion != null) {
                    pdfVersion = newPdfVersion;
                }
                writer.writeHeader();

                if (writer.crypto == null) {
                    writer.initCryptoIfSpecified(pdfVersion);
                }
                if (writer.crypto != null) {
                    if (!embeddedStreamsSavedOnReading && writer.crypto.isEmbeddedFilesOnly()) {
                        encryptedEmbeddedStreamsHandler.storeAllEmbeddedStreams();
                    }
                    writer.crypto.checkEncryptionRequirements(this);
                    writer.crypto.configureEncryptionParametersFromWriter(this);
                }
            }
            if (EventConfirmationType.ON_DEMAND == event.getConfirmationType()) {
                // Event confirmation: opening has passed successfully
                EventManager.getInstance().onEvent(new ConfirmEvent(event));
            }
        } catch (IOException e) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_OPEN_DOCUMENT, e, this);
        }
    }

    /**
     * Updates XMP metadata.
     * Shall be overridden.
     */
    protected void updateXmpMetadata() {
        try {
            // We add PDF producer info in any case, and the valid way to do it for PDF 2.0 in only in metadata, not
            // in the info dictionary.
            if (getXmpMetadataBytes() != null || writer.properties.addXmpMetadata
                    || pdfVersion.compareTo(PdfVersion.PDF_2_0) >= 0) {
                final XMPMeta xmpMeta = updateDefaultXmpMetadata();
                setXmpMetadata(xmpMeta);
            }
        } catch (XMPException e) {
            LOGGER.error(IoLogMessageConstant.EXCEPTION_WHILE_UPDATING_XMPMETADATA, e);
        }
    }

    /**
     * Update XMP metadata values from {@link PdfDocumentInfo}.
     *
     * @return the XMPMetadata
     *
     * @throws XMPException if the file is not well-formed XML or if parsing fails.
     */
    protected XMPMeta updateDefaultXmpMetadata() throws XMPException {
        XMPMeta xmpMeta = getXmpMetadata(true);
        XmpMetaInfoConverter.appendDocumentInfoToMetadata(getDocumentInfo(), xmpMeta);
        PdfConformance.setConformanceToXmp(xmpMeta, pdfConformance);
        return xmpMeta;
    }

    /**
     * List all newly added or loaded fonts
     *
     * @return List of {@link PdfFont}.
     */
    protected Collection<PdfFont> getDocumentFonts() {
        return documentFonts.values();
    }

    /**
     * Flushes all newly added or loaded fonts.
     */
    protected void flushFonts() {
        if (properties.appendMode) {
            for (PdfFont font : getDocumentFonts()) {
                if (font.getPdfObject().checkState(PdfObject.MUST_BE_INDIRECT) || font.getPdfObject()
                        .getIndirectReference().checkState(PdfObject.MODIFIED)) {
                    font.flush();
                }
            }
        } else {
            for (PdfFont font : getDocumentFonts()) {
                font.flush();
            }
        }
    }

    /**
     * Checks page before adding and add.
     *
     * @param index one-base index of the page.
     * @param page  {@link PdfPage} to add.
     */
    protected void checkAndAddPage(int index, PdfPage page) {
        if (page.isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.FLUSHED_PAGE_CANNOT_BE_ADDED_OR_INSERTED, page);
        }

        PdfDocument document = page.getDocument();
        if (document != null && this != document) {
            throw new PdfException(
                    KernelExceptionMessageConstant.
                            PAGE_CANNOT_BE_ADDED_TO_DOCUMENT_BECAUSE_IT_BELONGS_TO_ANOTHER_DOCUMENT).setMessageParams(
                    document, document.getPageNumber(page), this);
        }
        catalog.getPageTree().addPage(index, page);
    }

    /**
     * Checks page before adding.
     *
     * @param page {@link PdfPage} to add.
     */
    protected void checkAndAddPage(PdfPage page) {
        if (page.isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.FLUSHED_PAGE_CANNOT_BE_ADDED_OR_INSERTED, page);
        }
        if (page.getDocument() != null && this != page.getDocument()) {
            throw new PdfException(
                    KernelExceptionMessageConstant.
                            PAGE_CANNOT_BE_ADDED_TO_DOCUMENT_BECAUSE_IT_BELONGS_TO_ANOTHER_DOCUMENT).setMessageParams(
                    page.getDocument(), page.getDocument().getPageNumber(page), this);
        }
        catalog.getPageTree().addPage(page);
    }

    /**
     * checks whether a method is invoked at the closed document
     */
    protected void checkClosingStatus() {
        if (closed) {
            throw new PdfException(KernelExceptionMessageConstant.DOCUMENT_CLOSED_IT_IS_IMPOSSIBLE_TO_EXECUTE_ACTION);
        }
    }

    /**
     * Returns the factory for creating page instances.
     *
     * @return implementation of {@link IPdfPageFactory} for current document
     */
    protected IPdfPageFactory getPageFactory() {
        return pdfPageFactory;
    }

    /**
     * Initializes the new instance of document's structure tree root {@link PdfStructTreeRoot}.
     * See ISO 32000-1, section 14.7.2 Structure Hierarchy.
     *
     * @param str dictionary to create structure tree root
     */
    protected void tryInitTagStructure(PdfDictionary str) {
        try {
            structTreeRoot = new PdfStructTreeRoot(str, this);
            structParentIndex = getStructTreeRoot().getParentTreeNextKey();
        } catch (MemoryLimitsAwareException e){
            throw e;
        } catch (Exception e) {
            structTreeRoot = null;
            structParentIndex = -1;
            LOGGER.error(IoLogMessageConstant.TAG_STRUCTURE_INIT_FAILED, e);
        }
    }

    /**
     * Gets list of indirect references.
     *
     * @return list of indirect references.
     */
    PdfXrefTable getXref() {
        return xref;
    }

    boolean isDocumentFont(PdfIndirectReference indRef) {
        return indRef != null && documentFonts.containsKey(indRef);
    }

    boolean doesStreamBelongToEmbeddedFile(PdfStream stream) {
        return encryptedEmbeddedStreamsHandler.isStreamStoredAsEmbedded(stream);
    }

    boolean hasAcroForm() {
        return getCatalog().getPdfObject().containsKey(PdfName.AcroForm);
    }

    private void enableByteArrayWritingMode() {
        if (properties.appendMode || properties.preserveEncryption) {
            if (reader.decrypt != null && reader.decrypt.getMacContainer() != null) {
                writer.enableByteArrayWritingMode();
            }
        } else if (writer.properties.encryptionProperties != null &&
                writer.properties.encryptionProperties.macProperties != null &&
                writer.properties.pdfVersion != null &&
                PdfVersion.PDF_2_0.compareTo(writer.properties.pdfVersion) <= 0) {
            writer.enableByteArrayWritingMode();
        }
    }

    private void tryFlushTagStructure(boolean isAppendMode) {
        try {
            if (tagStructureContext != null) {
                tagStructureContext.prepareToDocumentClosing();
            }
            if (!isAppendMode || structTreeRoot.getPdfObject().isModified()) {
                structTreeRoot.flush();
            }
        } catch (MemoryLimitsAwareException e){
            throw e;
        } catch (Exception e) { 
            throw new PdfException(KernelExceptionMessageConstant.TAG_STRUCTURE_FLUSHING_FAILED_IT_MIGHT_BE_CORRUPTED,
                    e);
        }
    }

    private void updateValueInMarkInfoDict(PdfName key, PdfObject value) {
        PdfDictionary markInfo = catalog.getPdfObject().getAsDictionary(PdfName.MarkInfo);
        if (markInfo == null) {
            markInfo = new PdfDictionary();
            catalog.getPdfObject().put(PdfName.MarkInfo, markInfo);
        }
        markInfo.put(key, value);
    }

    /**
     * Removes all widgets associated with a given page from AcroForm structure. Widgets can be either pure or merged.
     *
     * @param page to remove from.
     */
    private void removeUnusedWidgetsFromFields(PdfPage page) {
        if (page.isFlushed()) {
            return;
        }

        final PdfDictionary acroForm = this.getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm);
        final PdfArray fields = acroForm == null ? null : acroForm.getAsArray(PdfName.Fields);

        List<PdfAnnotation> annots = page.getAnnotations();
        for (PdfAnnotation annot : annots) {
            if (annot.getSubtype().equals(PdfName.Widget)) {
                ((PdfWidgetAnnotation) annot).releaseFormFieldFromWidgetAnnotation();
                if (fields != null) {
                    fields.remove(annot.getPdfObject());
                }
            }
        }
    }

    private void resolveDestinations(PdfDocument toDocument, Map<PdfPage, PdfPage> page2page) {
        for (int i = 0; i < pendingDestinationMutations.size(); ++i) {
            PdfDocument.DestinationMutationInfo mutation = pendingDestinationMutations.get(i);
            PdfDestination copiedDest = null;
            copiedDest = getCatalog().copyDestination(mutation.getOriginalDestination().getPdfObject(), page2page,
                    toDocument);
            if (copiedDest == null) {
                mutation.handleDestinationUnavailable();
            } else {
                mutation.handleDestinationAvailable(copiedDest);
            }
        }
    }

    /**
     * This method copies all given outlines
     *
     * @param outlines   outlines to be copied
     * @param toDocument document where outlines should be copied
     */
    private void copyOutlines(Set<PdfOutline> outlines, PdfDocument toDocument, Map<PdfPage, PdfPage> page2page) {

        final Set<PdfOutline> outlinesToCopy = new HashSet<>();
        outlinesToCopy.addAll(outlines);

        for (PdfOutline outline : outlines) {
            getAllOutlinesToCopy(outline, outlinesToCopy);
        }

        PdfOutline rootOutline = toDocument.getOutlines(false);
        if (rootOutline == null) {
            rootOutline = new PdfOutline(toDocument);
            rootOutline.setTitle("Outlines");
        }

        cloneOutlines(outlinesToCopy, rootOutline, getOutlines(false), page2page, toDocument);
    }

    /**
     * This method gets all outlines to be copied including parent outlines
     *
     * @param outline        current outline
     * @param outlinesToCopy a Set of outlines to be copied
     */
    private void getAllOutlinesToCopy(PdfOutline outline, Set<PdfOutline> outlinesToCopy) {
        final PdfOutline parent = outline.getParent();
        //note there's no need to continue recursion if the current outline parent is root (first condition) or
        // if it is already in the Set of outlines to be copied (second condition)
        if ("Outlines".equals(parent.getTitle()) || outlinesToCopy.contains(parent)) {
            return;
        }
        outlinesToCopy.add(parent);
        getAllOutlinesToCopy(parent, outlinesToCopy);
    }

    /**
     * This method copies create new outlines in the Document to copy.
     *
     * @param outlinesToCopy - Set of outlines to be copied
     * @param newParent      - new parent outline
     * @param oldParent      - old parent outline
     */
    private void cloneOutlines(Set<PdfOutline> outlinesToCopy, PdfOutline newParent, PdfOutline oldParent,
            Map<PdfPage, PdfPage> page2page, PdfDocument toDocument) {
        if (null == oldParent) {
            return;
        }
        for (PdfOutline outline : oldParent.getAllChildren()) {
            if (outlinesToCopy.contains(outline)) {
                PdfDestination copiedDest = null;
                if (null != outline.getDestination()) {
                    PdfObject destObjToCopy = outline.getDestination().getPdfObject();
                    copiedDest = getCatalog().copyDestination(destObjToCopy, page2page, toDocument);
                }
                PdfOutline child = newParent.addOutline(outline.getTitle());
                if (copiedDest != null) {
                    child.addDestination(copiedDest);
                }
                Integer copiedStyle = outline.getStyle();
                if (copiedStyle != null) {
                    child.setStyle(copiedStyle.intValue());
                }
                Color copiedColor = outline.getColor();
                if (copiedColor != null) {
                    child.setColor(copiedColor);
                }
                child.setOpen(outline.isOpen());

                cloneOutlines(outlinesToCopy, child, outline, page2page, toDocument);
            }
        }
    }

    private void ensureTreeRootAddedToNames(PdfObject treeRoot, PdfName treeType) {
        PdfDictionary names = catalog.getPdfObject().getAsDictionary(PdfName.Names);
        if (names == null) {
            names = new PdfDictionary();
            catalog.put(PdfName.Names, names);
            names.makeIndirect(this);
        }
        names.put(treeType, treeRoot);
        names.setModified();
    }

    private boolean writerHasEncryption() {
        return writer.properties.isStandardEncryptionUsed() || writer.properties.isPublicKeyEncryptionUsed();
    }

    private void updatePdfVersionFromCatalog() {
        if (catalog.getPdfObject().containsKey(PdfName.Version)) {
            // The version of the PDF specification to which the document conforms (for example, 1.4)
            // if later than the version specified in the file's header
            try {
                PdfVersion catalogVersion = PdfVersion.fromPdfName(catalog.getPdfObject().getAsName(PdfName.Version));
                if (catalogVersion.compareTo(pdfVersion) > 0) {
                    pdfVersion = catalogVersion;
                }
            } catch (IllegalArgumentException e) {
                processReadingError(IoLogMessageConstant.DOCUMENT_VERSION_IN_CATALOG_CORRUPTED);
            }
        }
    }

    private void readDocumentIds() {
        final PdfArray id = reader.trailer.getAsArray(PdfName.ID);

        if (id != null) {
            if (id.size() == 2) {
                originalDocumentId = id.getAsString(0);
                modifiedDocumentId = id.getAsString(1);
            }

            if (originalDocumentId == null || modifiedDocumentId == null) {
                processReadingError(IoLogMessageConstant.DOCUMENT_IDS_ARE_CORRUPTED);
            }
        }
    }

    private void processReadingError(String errorMessage) {
        if (StrictnessLevel.CONSERVATIVE.isStricter(reader.getStrictnessLevel())) {
            LOGGER.error(errorMessage);
        } else {
            throw new PdfException(errorMessage);
        }
    }

    private static void overrideFullCompressionInWriterProperties(WriterProperties properties,
            boolean readerHasXrefStream) {
        if (Boolean.TRUE == properties.isFullCompression && !readerHasXrefStream) {
            LOGGER.warn(KernelLogMessageConstant.FULL_COMPRESSION_APPEND_MODE_XREF_TABLE_INCONSISTENCY);
        } else if (Boolean.FALSE == properties.isFullCompression && readerHasXrefStream) {
            LOGGER.warn(KernelLogMessageConstant.FULL_COMPRESSION_APPEND_MODE_XREF_STREAM_INCONSISTENCY);
        }
        properties.isFullCompression = readerHasXrefStream;
    }

    private static class DestinationMutationInfo {
        private final PdfDestination originalDestination;
        private final Consumer<PdfDestination> onDestinationAvailable;
        private final Consumer<PdfDestination> onDestinationNotAvailable;

        public DestinationMutationInfo(PdfDestination originalDestination,
                Consumer<PdfDestination> onDestinationAvailable, Consumer<PdfDestination> onDestinationNotAvailable) {
            this.originalDestination = originalDestination;
            this.onDestinationAvailable = onDestinationAvailable;
            this.onDestinationNotAvailable = onDestinationNotAvailable;
        }

        public void handleDestinationAvailable(PdfDestination newDestination) {
            onDestinationAvailable.accept(newDestination);
        }

        public void handleDestinationUnavailable() {
            onDestinationNotAvailable.accept(originalDestination);
        }

        public PdfDestination getOriginalDestination() {
            return originalDestination;
        }
    }
}