PdfPage.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.utils.MessageFormatUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.action.PdfAction;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfPrinterMarkAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.kernel.pdf.event.PdfDocumentEvent;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.kernel.pdf.tagging.PdfNamespace;
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
import com.itextpdf.kernel.utils.ICopyFilter;
import com.itextpdf.kernel.utils.NullCopyFilter;
import com.itextpdf.kernel.utils.checkers.PdfCheckersUtil;
import com.itextpdf.kernel.validation.context.PdfAnnotationContext;
import com.itextpdf.kernel.validation.context.PdfDestinationAdditionContext;
import com.itextpdf.kernel.validation.context.PdfPageValidationContext;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class PdfPage extends PdfObjectWrapper<PdfDictionary> {

    private PdfResources resources = null;
    private int mcid = -1;
    PdfPages parentPages;
    private static final List<PdfName> PAGE_EXCLUDED_KEYS = new ArrayList<>(Arrays.asList(
            PdfName.Parent,
            PdfName.Annots,
            PdfName.StructParents,
            // This key contains reference to all articles, while this articles could reference to lots of pages.
            // See DEVSIX-191
            PdfName.B));

    private static final List<PdfName> XOBJECT_EXCLUDED_KEYS;

    static {
        XOBJECT_EXCLUDED_KEYS = new ArrayList<>(Arrays.asList(PdfName.MediaBox,
                PdfName.CropBox,
                PdfName.TrimBox,
                PdfName.Contents));
        XOBJECT_EXCLUDED_KEYS.addAll(PAGE_EXCLUDED_KEYS);
    }

    /**
     * Automatically rotate new content if the page has a rotation ( is disabled by default )
     */
    private boolean ignorePageRotationForContent = false;

    /**
     * See {@link #isPageRotationInverseMatrixWritten()}.
     */
    private boolean pageRotationInverseMatrixWritten = false;

    protected PdfPage(PdfDictionary pdfObject) {
        super(pdfObject);
        setForbidRelease();
        ensureObjectIsAddedToDocument(pdfObject);
    }

    protected PdfPage(PdfDocument pdfDocument, PageSize pageSize) {
        this((PdfDictionary) new PdfDictionary().makeIndirect(pdfDocument));
        PdfStream contentStream = (PdfStream) new PdfStream().makeIndirect(pdfDocument);
        getPdfObject().put(PdfName.Contents, contentStream);
        getPdfObject().put(PdfName.Type, PdfName.Page);
        getPdfObject().put(PdfName.MediaBox, new PdfArray(pageSize));
        getPdfObject().put(PdfName.TrimBox, new PdfArray(pageSize));
        if (pdfDocument.isTagged()) {
            setTabOrder(PdfName.S);
        }
    }

    protected PdfPage(PdfDocument pdfDocument) {
        this(pdfDocument, pdfDocument.getDefaultPageSize());
    }

    /**
     * Gets page size, defined by media box object. This method doesn't take page rotation into account.
     *
     * @return {@link Rectangle} that specify page size.
     */
    public Rectangle getPageSize() {
        return getMediaBox();
    }

    /**
     * Gets page size, considering page rotation.
     *
     * @return {@link Rectangle} that specify size of rotated page.
     */
    public Rectangle getPageSizeWithRotation() {
        PageSize rect = new PageSize(getPageSize());
        int rotation = getRotation();
        while (rotation > 0) {
            rect = rect.rotate();
            rotation -= 90;
        }
        return rect;
    }

    /**
     * Gets the number of degrees by which the page shall be rotated clockwise when displayed or printed.
     * Shall be a multiple of 90.
     *
     * @return {@code int} number of degrees. Default value: 0
     */
    public int getRotation() {
        PdfNumber rotate = getPdfObject().getAsNumber(PdfName.Rotate);
        int rotateValue = 0;
        if (rotate == null) {
            rotate = (PdfNumber) getInheritedValue(PdfName.Rotate, PdfObject.NUMBER);
        }
        if (rotate != null) {
            rotateValue = rotate.intValue();
        }
        rotateValue %= 360;
        return rotateValue < 0 ? rotateValue + 360 : rotateValue;
    }

    /**
     * Sets the page rotation.
     *
     * @param degAngle the {@code int}  number of degrees by which the page shall be rotated clockwise
     *                 when displayed or printed. Shall be a multiple of 90.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setRotation(int degAngle) {
        put(PdfName.Rotate, new PdfNumber(degAngle));
        return this;
    }

    /**
     * Gets the content stream at specified 0-based index in the Contents object {@link PdfArray}.
     * The situation when Contents object is a {@link PdfStream} is treated like a one element array.
     *
     * @param index the {@code int} index of returned {@link PdfStream}.
     * @return {@link PdfStream} object at specified index;
     * will return null in case page dictionary doesn't adhere to the specification, meaning that the document is an invalid PDF.
     * @throws IndexOutOfBoundsException if the index is out of range
     */
    public PdfStream getContentStream(int index) {
        int count = getContentStreamCount();
        if (index >= count || index < 0)
            throw new IndexOutOfBoundsException(MessageFormatUtil.format("Index: {0}, Size: {1}", index, count));
        PdfObject contents = getPdfObject().get(PdfName.Contents);
        if (contents instanceof PdfStream)
            return (PdfStream) contents;
        else if (contents instanceof PdfArray) {
            PdfArray a = (PdfArray) contents;
            return a.getAsStream(index);
        } else {
            return null;
        }
    }

    /**
     * Gets the size of Contents object {@link PdfArray}.
     * The situation when Contents object is a {@link PdfStream} is treated like a one element array.
     *
     * @return the {@code int} size of Contents object, or 1 if Contents object is a {@link PdfStream}.
     */
    public int getContentStreamCount() {
        PdfObject contents = getPdfObject().get(PdfName.Contents);
        if (contents instanceof PdfStream)
            return 1;
        else if (contents instanceof PdfArray) {
            return ((PdfArray) contents).size();
        } else {
            return 0;
        }
    }

    /**
     * Returns the Contents object if it is {@link PdfStream}, or first stream in the array if it is {@link PdfArray}.
     *
     * @return first {@link PdfStream} in Contents object, or {@code null} if Contents is empty.
     */
    public PdfStream getFirstContentStream() {
        if (getContentStreamCount() > 0)
            return getContentStream(0);
        return null;
    }

    /**
     * Returns the Contents object if it is {@link PdfStream}, or last stream in the array if it is {@link PdfArray}.
     *
     * @return first {@link PdfStream} in Contents object, or {@code null} if Contents is empty.
     */
    public PdfStream getLastContentStream() {
        int count = getContentStreamCount();
        if (count > 0)
            return getContentStream(count - 1);
        return null;
    }

    /**
     * Creates new {@link PdfStream} object and puts it at the beginning of Contents array
     * (if Contents object is {@link PdfStream} it will be replaced with one-element array).
     *
     * @return Created {@link PdfStream} object.
     */
    public PdfStream newContentStreamBefore() {
        return newContentStream(true);
    }

    /**
     * Creates new {@link PdfStream} object and puts it at the end of {@code Contents} array
     * (if Contents object is {@link PdfStream} it will be replaced with one-element array).
     *
     * @return Created {@link PdfStream} object.
     */
    public PdfStream newContentStreamAfter() {
        return newContentStream(false);
    }

    /**
     * Gets the {@link PdfResources} wrapper object for this page resources.
     * If page doesn't have resource object, then it will be inherited from page's parents.
     * If neither parents nor page has the resource object, then the new one is created and added to page dictionary.
     * <br><br>
     * NOTE: If you'll try to modify the inherited resources, then the new resources object will be created,
     * so you won't change the parent's resources.
     * This new object under the wrapper will be added to page dictionary on {@link PdfPage#flush()},
     * or you can add it manually with this line, if needed:<br>
     * {@code getPdfObject().put(PdfName.Resources, getResources().getPdfObject());}
     *
     * @return {@link PdfResources} wrapper of the page.
     */
    public PdfResources getResources() {
        return getResources(true);
    }

    PdfResources getResources(boolean initResourcesField) {
        if (this.resources == null && initResourcesField) {
            initResources(true);
        }
        return this.resources;
    }

    PdfDictionary initResources(boolean initResourcesField) {
        boolean readOnly = false;
        PdfDictionary resources = getPdfObject().getAsDictionary(PdfName.Resources);
        if (resources == null) {
            resources = (PdfDictionary) getInheritedValue(PdfName.Resources, PdfObject.DICTIONARY);
            if (resources != null) {
                readOnly = true;
            }
        }
        if (resources == null) {
            resources = new PdfDictionary();
            // not marking page as modified because of this change
            getPdfObject().put(PdfName.Resources, resources);
        }
        if (initResourcesField) {
            this.resources = new PdfResources(resources);
            this.resources.setReadOnly(readOnly);
        }
        return resources;
    }

    /**
     * Sets {@link PdfResources} object.
     *
     * @param pdfResources {@link PdfResources} to set.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setResources(PdfResources pdfResources) {
        put(PdfName.Resources, pdfResources.getPdfObject());
        this.resources = pdfResources;
        return this;
    }

    /**
     * Sets the XMP Metadata.
     *
     * @param xmpMetadata the {@code byte[]} of XMP Metadata to set.
     * @return this {@link PdfPage} instance.
     * @throws IOException in case of writing error.
     */
    public PdfPage setXmpMetadata(byte[] xmpMetadata) throws IOException {
        PdfStream xmp = (PdfStream) new PdfStream().makeIndirect(getDocument());
        xmp.getOutputStream().write(xmpMetadata);
        xmp.put(PdfName.Type, PdfName.Metadata);
        xmp.put(PdfName.Subtype, PdfName.XML);
        put(PdfName.Metadata, xmp);
        return this;
    }

    /**
     * Serializes XMP Metadata to byte array and sets it.
     *
     * @param xmpMeta          the {@link XMPMeta} object to set.
     * @param serializeOptions the {@link SerializeOptions} used while serialization.
     * @return this {@link PdfPage} instance.
     * @throws XMPException in case of XMP Metadata serialization error.
     * @throws IOException  in case of writing error.
     */
    public PdfPage setXmpMetadata(XMPMeta xmpMeta, SerializeOptions serializeOptions) throws XMPException, IOException {
        return setXmpMetadata(XMPMetaFactory.serializeToBuffer(xmpMeta, serializeOptions));
    }

    /**
     * Serializes XMP Metadata to byte array and sets it. Uses padding equals to 2000.
     *
     * @param xmpMeta the {@link XMPMeta} object to set.
     * @return this {@link PdfPage} instance.
     * @throws XMPException in case of XMP Metadata serialization error.
     * @throws IOException  in case of writing error.
     */
    public PdfPage setXmpMetadata(XMPMeta xmpMeta) throws XMPException, IOException {
        SerializeOptions serializeOptions = new SerializeOptions();
        serializeOptions.setPadding(2000);
        return setXmpMetadata(xmpMeta, serializeOptions);
    }

    /**
     * Gets the XMP Metadata object.
     *
     * @return {@link PdfStream} object, that represent XMP Metadata.
     */
    public PdfStream getXmpMetadata() {
        return getPdfObject().getAsStream(PdfName.Metadata);
    }

    /**
     * Copies page to the specified document.
     * <br><br>
     * NOTE: Works only for pages from the document opened in reading mode, otherwise an exception is thrown.
     *
     * @param toDocument a document to copy page to.
     * @return copied {@link PdfPage}.
     */
    public PdfPage copyTo(PdfDocument toDocument) {
        return copyTo(toDocument, null);
    }

    /**
     * Copies page to the specified document.
     * <br><br>
     * NOTE: Works only for pages from the document opened in reading mode, otherwise an exception is thrown.
     *
     * @param toDocument a document to copy page 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 copied {@link PdfPage}.
     */
    public PdfPage copyTo(PdfDocument toDocument, IPdfPageExtraCopier copier) {
        return copyTo(toDocument, copier, false, -1);
    }

    /**
     * Copies page and adds it to the specified document to the end or by index if the corresponding parameter is true.
     * <br><br>
     * NOTE: Works only for pages from the document opened in reading mode, otherwise an exception is thrown.
     * NOTE: If both documents (from which and to which the copy is made) are tagged, you must additionally call the
     * {@link IPdfPageFormCopier#recreateAcroformToProcessCopiedFields(PdfDocument)} method after copying the
     * tag structure to process copied fields, like add them to the document and merge fields with the same names.
     *
     * @param toDocument a document to copy page 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.
     * @param addPageToDocument true if page should be added to document.
     * @param pageInsertIndex position to add the page to, if -1 page will be added to the end of the document,
     *                        will be ignored if addPageToDocument is false.
     *
     * @return copied {@link PdfPage}.
     */
    public PdfPage copyTo(PdfDocument toDocument, IPdfPageExtraCopier copier,
                          boolean addPageToDocument, int pageInsertIndex) {
        final ICopyFilter copyFilter = new DestinationResolverCopyFilter(this.getDocument(), toDocument);
        final PdfDictionary dictionary =
                getPdfObject().copyTo(toDocument, PAGE_EXCLUDED_KEYS, true, copyFilter);
        PdfPage page = getDocument().getPageFactory().createPdfPage(dictionary);
        if (addPageToDocument) {
            if (pageInsertIndex == -1) {
                toDocument.addPage(page);
            } else {
                toDocument.addPage(pageInsertIndex, page);
            }
        }
        return copyTo(page, toDocument, copier);
    }

    /**
     * Get all pdf layers stored under this page's annotations/xobjects/resources.
     * Note that it will include all layers, even those already stored under /OCProperties entry in catalog.
     * To get only unique layers, you can simply exclude ocgs, which already present in catalog.
     *
     * @return set of pdf layers, associated with this page.
     */
    public Set<PdfLayer> getPdfLayers() {
        Set<PdfIndirectReference> ocgs = OcgPropertiesCopier.getOCGsFromPage(this);
        Set<PdfLayer> result = new LinkedHashSet<>();
        for (PdfIndirectReference ocg : ocgs) {
            if (ocg.getRefersTo() != null && ocg.getRefersTo().isDictionary()) {
                result.add(new PdfLayer((PdfDictionary) ocg.getRefersTo()));
            }
        }
        return result;
    }

    /**
     * Copies page as FormXObject to the specified document.
     *
     * @param toDocument a document to copy to.
     * @return copied {@link PdfFormXObject} object.
     * @throws IOException if an I/O error occurs.
     */
    public PdfFormXObject copyAsFormXObject(PdfDocument toDocument) throws IOException {
        PdfFormXObject xObject = new PdfFormXObject(getCropBox());

        for (PdfName key : getPdfObject().keySet()) {
            if (XOBJECT_EXCLUDED_KEYS.contains(key)) {
                continue;
            }
            PdfObject obj = getPdfObject().get(key);
            if (!xObject.getPdfObject().containsKey(key)) {
                final PdfObject copyObj = obj.copyTo(toDocument, false, NullCopyFilter.getInstance());
                xObject.getPdfObject().put(key, copyObj);
            }
        }
        xObject.getPdfObject().getOutputStream().write(getContentBytes());
        //Copy inherited resources
        if (!xObject.getPdfObject().containsKey(PdfName.Resources)) {
            final PdfObject copyResource = getResources().getPdfObject().copyTo(toDocument, true,
                    NullCopyFilter.getInstance());
            xObject.getPdfObject().put(PdfName.Resources, copyResource);
        }

        return xObject;
    }

    /**
     * Gets the {@link PdfDocument} that owns that page, or {@code null} if such document isn't exist.
     *
     * @return {@link PdfDocument} that owns that page, or {@code null} if such document isn't exist.
     */
    public PdfDocument getDocument() {
        if (getPdfObject().getIndirectReference() != null)
            return getPdfObject().getIndirectReference().getDocument();
        return null;
    }

    /**
     * Flushes page dictionary, its content streams, annotations and thumb image.
     * <p>
     * If the page belongs to the document which is tagged, page flushing also triggers flushing of the tags,
     * which are considered to belong to the page. The logic that defines if the given tag (structure element) belongs
     * to the page is the following: if all the marked content references (dictionary or number references), that are the
     * descendants of the given structure element, belong to the current page - the tag is considered
     * to belong to the page. If tag has descendants from several pages - it is flushed, if all other pages except the
     * current one are flushed.
     */
    @Override
    public void flush() {
        flush(false);
    }

    /**
     * Flushes page dictionary, its content streams, annotations and thumb image. If <code>flushResourcesContentStreams</code> is true,
     * all content streams that are rendered on this page (like FormXObjects, annotation appearance streams, patterns)
     * and also all images associated with this page will also be flushed.
     * <p>
     * For notes about tag structure flushing see {@link PdfPage#flush() PdfPage#flush() method}.
     * <p>
     * If <code>PdfADocument</code> is used, flushing will be applied only if <code>flushResourcesContentStreams</code> is true.
     * <p>
     * Be careful with handling document in which some of the pages are flushed. Keep in mind that flushed objects are
     * finalized and are completely written to the output stream. This frees their memory but makes
     * it impossible to modify or read data from them. Whenever there is an attempt to modify or to fetch
     * flushed object inner contents an exception will be thrown. Flushing is only possible for objects in the writing
     * and stamping modes, also its possible to flush modified objects in append mode.
     *
     * @param flushResourcesContentStreams if true all content streams that are rendered on this page (like form xObjects,
     *                                     annotation appearance streams, patterns) and also all images associated with this page
     *                                     will be flushed.
     */
    public void flush(boolean flushResourcesContentStreams) {
        if (isFlushed()) {
            return;
        }
        getDocument().dispatchEvent(new PdfDocumentEvent(PdfDocumentEvent.END_PAGE, this));

        if (getDocument().isTagged() && !getDocument().getStructTreeRoot().isFlushed()) {
            tryFlushPageTags();
        }

        if (resources == null) {
            // ensure that either resources are inherited or add empty resources dictionary
            initResources(false);
        } else if (resources.isModified() && !resources.isReadOnly()) {
            put(PdfName.Resources, resources.getPdfObject());
        }
        if (flushResourcesContentStreams) {
            getDocument().checkIsoConformance(new PdfPageValidationContext(this));
            flushResourcesContentStreams();
        }

        PdfArray annots = getAnnots(false);

        if (annots != null && !annots.isFlushed()) {
            for (int i = 0; i < annots.size(); ++i) {
                PdfObject a = annots.get(i);
                if (a != null) {
                    a.makeIndirect(getDocument()).flush();
                }
            }
        }

        PdfStream thumb = getPdfObject().getAsStream(PdfName.Thumb);
        if (thumb != null) {
            thumb.flush();
        }

        PdfObject contentsObj = getPdfObject().get(PdfName.Contents);
        // avoid trying to operate with flushed /Contents array
        if (contentsObj != null && !contentsObj.isFlushed()) {
            int contentStreamCount = getContentStreamCount();
            for (int i = 0; i < contentStreamCount; i++) {
                PdfStream contentStream = getContentStream(i);
                if (contentStream != null) {
                    contentStream.flush(false);
                }
            }
        }
        releaseInstanceFields();

        super.flush();
    }

    /**
     * Gets {@link Rectangle} object specified by page's Media Box, that defines the boundaries of the physical medium
     * on which the page shall be displayed or printed
     *
     * @return {@link Rectangle} object specified by page Media Box, expressed in default user space units.
     * @throws PdfException in case of any error while reading MediaBox object.
     */
    public Rectangle getMediaBox() {
        PdfArray mediaBox = getPdfObject().getAsArray(PdfName.MediaBox);
        if (mediaBox == null) {
            mediaBox = (PdfArray) getInheritedValue(PdfName.MediaBox, PdfObject.ARRAY);
        }
        if (mediaBox == null) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RETRIEVE_MEDIA_BOX_ATTRIBUTE);
        }
        int mediaBoxSize;
        if ((mediaBoxSize = mediaBox.size()) != 4) {
            if (mediaBoxSize > 4) {
                Logger logger = LoggerFactory.getLogger(PdfPage.class);
                if (logger.isErrorEnabled()) {
                    logger.error(MessageFormatUtil.format(IoLogMessageConstant.WRONG_MEDIABOX_SIZE_TOO_MANY_ARGUMENTS,
                            mediaBoxSize));

                }
            }
            if (mediaBoxSize < 4) {
                throw new PdfException(KernelExceptionMessageConstant. WRONG_MEDIA_BOX_SIZE_TOO_FEW_ARGUMENTS)
                        .setMessageParams(mediaBox.size());
            }
        }

        PdfNumber llx = mediaBox.getAsNumber(0);
        PdfNumber lly = mediaBox.getAsNumber(1);
        PdfNumber urx = mediaBox.getAsNumber(2);
        PdfNumber ury = mediaBox.getAsNumber(3);
        if (llx == null || lly == null || urx == null || ury == null) {
            throw new PdfException(KernelExceptionMessageConstant.INVALID_MEDIA_BOX_VALUE);
        }
        return new Rectangle(Math.min(llx.floatValue(), urx.floatValue()),
                Math.min(lly.floatValue(), ury.floatValue()),
                Math.abs(urx.floatValue() - llx.floatValue()),
                Math.abs(ury.floatValue() - lly.floatValue()));
    }

    /**
     * Sets the Media Box object, that defines the boundaries of the physical medium
     * on which the page shall be displayed or printed.
     *
     * @param rectangle the {@link Rectangle} object to set, expressed in default user space units.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setMediaBox(Rectangle rectangle) {
        put(PdfName.MediaBox, new PdfArray(rectangle));
        return this;
    }

    /**
     * Gets the {@link Rectangle} specified by page's CropBox, that defines the visible region of default user space.
     * When the page is displayed or printed, its contents shall be clipped (cropped) to this rectangle
     * and then shall be imposed on the output medium in some implementation-defined manner.
     *
     * @return the {@link Rectangle} object specified by pages's CropBox, expressed in default user space units.
     * MediaBox by default.
     */
    public Rectangle getCropBox() {
        PdfArray cropBox = getPdfObject().getAsArray(PdfName.CropBox);
        if (cropBox == null) {
            cropBox = (PdfArray) getInheritedValue(PdfName.CropBox, PdfObject.ARRAY);
            if (cropBox == null) {
                return getMediaBox();
            }
        }
        return cropBox.toRectangle();
    }

    /**
     * Sets the CropBox object, that defines the visible region of default user space.
     * When the page is displayed or printed, its contents shall be clipped (cropped) to this rectangle
     * and then shall be imposed on the output medium in some implementation-defined manner.
     *
     * @param rectangle the {@link Rectangle} object to set, expressed in default user space units.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setCropBox(Rectangle rectangle) {
        put(PdfName.CropBox, new PdfArray(rectangle));
        return this;
    }

    /**
     * Sets the BleedBox object, that defines the region to which the contents of the page shall be clipped
     * when output in a production environment.
     *
     * @param rectangle the {@link Rectangle} object to set, expressed in default user space units.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setBleedBox(Rectangle rectangle) {
        put(PdfName.BleedBox, new PdfArray(rectangle));
        return this;
    }

    /**
     * Gets the {@link Rectangle} object specified by page's BleedBox, that define the region to which the
     * contents of the page shall be clipped when output in a production environment.
     *
     * @return the {@link Rectangle} object specified by page's BleedBox, expressed in default user space units.
     * CropBox by default.
     */
    public Rectangle getBleedBox() {
        Rectangle bleedBox = getPdfObject().getAsRectangle(PdfName.BleedBox);
        return bleedBox == null ? getCropBox() : bleedBox;
    }

    /**
     * Sets the ArtBox object, that define the extent of the page���s meaningful content
     * (including potential white space) as intended by the page���s creator.
     *
     * @param rectangle the {@link Rectangle} object to set, expressed in default user space units.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setArtBox(Rectangle rectangle) {
        put(PdfName.ArtBox, new PdfArray(rectangle));
        return this;
    }

    /**
     * Gets the {@link Rectangle} object specified by page's ArtBox, that define the extent of the page���s
     * meaningful content (including potential white space) as intended by the page���s creator.
     *
     * @return the {@link Rectangle} object specified by page's ArtBox, expressed in default user space units.
     * CropBox by default.
     */
    public Rectangle getArtBox() {
        Rectangle artBox = getPdfObject().getAsRectangle(PdfName.ArtBox);
        return artBox == null ? getCropBox() : artBox;
    }

    /**
     * Sets the TrimBox object, that define the intended dimensions of the finished page after trimming.
     *
     * @param rectangle the {@link Rectangle} object to set, expressed in default user space units.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setTrimBox(Rectangle rectangle) {
        put(PdfName.TrimBox, new PdfArray(rectangle));
        return this;
    }

    /**
     * Gets the {@link Rectangle} object specified by page's TrimBox object,
     * that define the intended dimensions of the finished page after trimming.
     *
     * @return the {@link Rectangle} object specified by page's TrimBox, expressed in default user space units.
     * CropBox by default.
     */
    public Rectangle getTrimBox() {
        Rectangle trimBox = getPdfObject().getAsRectangle(PdfName.TrimBox);
        return trimBox == null ? getCropBox() : trimBox;
    }

    /**
     * Get decoded bytes for the whole page content.
     *
     * @return byte array.
     * @throws PdfException in case of any {@link IOException}.
     */
    public byte[] getContentBytes() {
        try {
            MemoryLimitsAwareHandler handler = getDocument().memoryLimitsAwareHandler;
            long usedMemory = null == handler ? -1 : handler.getAllMemoryUsedForDecompression();

            MemoryLimitsAwareOutputStream baos = new MemoryLimitsAwareOutputStream();
            int streamCount = getContentStreamCount();
            byte[] streamBytes;
            for (int i = 0; i < streamCount; i++) {
                streamBytes = getStreamBytes(i);
                // usedMemory has changed, that means that some of currently processed pdf streams are suspicious
                if (null != handler && usedMemory < handler.getAllMemoryUsedForDecompression()) {
                    baos.setMaxStreamSize(handler.getMaxSizeOfSingleDecompressedPdfStream());
                }
                baos.write(streamBytes);
                if (0 != streamBytes.length && !Character.isWhitespace((char) streamBytes[streamBytes.length - 1])) {
                    baos.write('\n');
                }
            }
            return baos.toByteArray();
        } catch (IOException ioe) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_GET_CONTENT_BYTES, ioe, this);
        }
    }

    /**
     * Gets decoded bytes of a certain stream of a page content.
     *
     * @param index index of stream inside Content.
     * @return byte array.
     * @throws PdfException in case of any {@link IOException}.
     */
    public byte[] getStreamBytes(int index) {
        return getContentStream(index).getBytes();
    }

    /**
     * Calculates and returns the next available for this page's content stream MCID reference.
     *
     * @return calculated MCID reference.
     * @throws PdfException in case of not tagged document.
     */
    public int getNextMcid() {
        if (!getDocument().isTagged()) {
            throw new PdfException(KernelExceptionMessageConstant.MUST_BE_A_TAGGED_DOCUMENT);
        }
        if (mcid == -1) {
            PdfStructTreeRoot structTreeRoot = getDocument().getStructTreeRoot();
            mcid = structTreeRoot.getNextMcidForPage(this);
        }
        return mcid++;
    }

    /**
     * Gets the key of the page���s entry in the structural parent tree.
     *
     * @return the key of the page���s entry in the structural parent tree.
     * If page has no entry in the structural parent tree, returned value is -1.
     */
    public int getStructParentIndex() {
        return getPdfObject().getAsNumber(PdfName.StructParents) != null ? getPdfObject().getAsNumber(PdfName.StructParents).intValue() : -1;
    }

    /**
     * Helper method to add an additional action to this page.
     * May be used in chain.
     *
     * @param key    a {@link PdfName} specifying the name of an additional action
     * @param action the {@link PdfAction} to add as an additional action
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setAdditionalAction(PdfName key, PdfAction action) {
        PdfAction.setAdditionalAction(this, key, action);
        return this;
    }

    /**
     * Gets array of annotation dictionaries that shall contain indirect references
     * to all annotations associated with the page.
     *
     * @return the {@link List}&lt;{@link PdfAnnotation}&gt; containing all page's annotations.
     */
    public List<PdfAnnotation> getAnnotations() {
        List<PdfAnnotation> annotations = new ArrayList<>();
        PdfArray annots = getPdfObject().getAsArray(PdfName.Annots);
        if (annots != null) {
            for (int i = 0; i < annots.size(); i++) {
                PdfDictionary annot = annots.getAsDictionary(i);
                if (annot == null) {
                    continue;
                }
                PdfAnnotation annotation = PdfAnnotation.makeAnnotation(annot);
                if (annotation == null) {
                    continue;
                }
                boolean hasBeenNotModified = annot.getIndirectReference() != null && !annot.getIndirectReference().checkState(PdfObject.MODIFIED);
                annotations.add(annotation.setPage(this));
                if (hasBeenNotModified) {
                    annot.getIndirectReference().clearState(PdfObject.MODIFIED);
                    annot.clearState(PdfObject.FORBID_RELEASE);
                }
            }
        }
        return annotations;
    }

    /**
     * Checks if page contains the specified annotation.
     *
     * @param annotation the {@link PdfAnnotation} to check.
     * @return {@code true} if page contains specified annotation and {@code false} otherwise.
     */
    public boolean containsAnnotation(PdfAnnotation annotation) {
        for (PdfAnnotation a : getAnnotations()) {
            if (a.getPdfObject().equals(annotation.getPdfObject())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adds specified annotation to the end of annotations array and tagged it.
     * May be used in chain.
     *
     * @param annotation the {@link PdfAnnotation} to add.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage addAnnotation(PdfAnnotation annotation) {
        return addAnnotation(-1, annotation, true);
    }

    /**
     * Adds specified {@link PdfAnnotation} to specified index in annotations array with or without autotagging.
     * May be used in chain.
     *
     * @param index         the index at which specified annotation will be added. If {@code -1} then annotation will be added
     *                      to the end of array.
     * @param annotation    the {@link PdfAnnotation} to add.
     * @param tagAnnotation if {@code true} the added annotation will be autotagged. <p>
     *                      (see {@link com.itextpdf.kernel.pdf.tagutils.TagStructureContext#getAutoTaggingPointer()})
     * @return this {@link PdfPage} instance.
     */
    public PdfPage addAnnotation(int index, PdfAnnotation annotation, boolean tagAnnotation) {
        if (getDocument().isTagged()) {
            if (tagAnnotation) {
                tagAnnotation(annotation);
            }
            if (getTabOrder() == null) {
                setTabOrder(PdfName.S);
            }
        }

        PdfArray annots = getAnnots(true);
        if (index == -1) {
            annots.add(annotation.setPage(this).getPdfObject());
        } else {
            annots.add(index, annotation.setPage(this).getPdfObject());
        }

        if (annots.getIndirectReference() == null) {
            //Annots are not indirect so page needs to be marked as modified
            setModified();
        } else {
            //Annots are indirect so need to be marked as modified
            annots.setModified();
        }
        checkIsoConformanceForAnnotation(annotation);

        return this;
    }

    /**
     * Removes an annotation from the page.
     *
     * <p>
     * When document is tagged a corresponding logical structure content item for this annotation
     * will be removed; its immediate structure element parent will be removed as well if the following
     * conditions are met: annotation content item was its single child and structure element role
     * is either Annot or Form.
     *
     * @param annotation an annotation to be removed
     * @return this {@link PdfPage} instance.
     */
    public PdfPage removeAnnotation(PdfAnnotation annotation) {
        return removeAnnotation(annotation, false);
    }

    /**
     * Removes an annotation from the page.
     *
     * <p>
     * When document is tagged a corresponding logical structure content item for this annotation
     * will be removed; its immediate structure element parent will be removed as well if the following
     * conditions are met: annotation content item was its single child and structure element role
     * is either Annot or Form.
     *
     * @param annotation         an annotation to be removed
     * @param rememberTagPointer if set to true, the {@link TagStructureContext#getAutoTaggingPointer()}
     *                           instance of {@link TagTreePointer} will be moved to the parent of the removed
     *                           annotation tag. Can be used to add a new annotation to the same place in the
     *                           tag structure. (E.g. when merged Acroform field is split into a field and
     *                           a pure widget, the page annotation needs to be replaced by the new one)
     *
     * @return this {@link PdfPage} instance.
     */
    public PdfPage removeAnnotation(PdfAnnotation annotation, boolean rememberTagPointer) {
        PdfArray annots = getAnnots(false);
        if (annots != null) {
            annots.remove(annotation.getPdfObject());

            if (annots.isEmpty()) {
                remove(PdfName.Annots);
            } else if (annots.getIndirectReference() == null) {
                setModified();
            } else {
                annots.setModified();
            }
        }

        if (getDocument().isTagged()) {
            TagTreePointer tagPointer = getDocument().getTagStructureContext()
                    .removeAnnotationTag(annotation, rememberTagPointer);
            if (tagPointer != null) {
                boolean standardAnnotTagRole = StandardRoles.ANNOT.equals(tagPointer.getRole())
                        || StandardRoles.FORM.equals(tagPointer.getRole());
                if (tagPointer.getKidsRoles().isEmpty() && standardAnnotTagRole) {
                    tagPointer.removeTag();
                }
            }
        }
        return this;
    }

    /**
     * Gets the number of {@link PdfAnnotation} associated with this page.
     *
     * @return the {@code int} number of {@link PdfAnnotation} associated with this page.
     */
    public int getAnnotsSize() {
        PdfArray annots = getAnnots(false);
        if (annots == null)
            return 0;
        return annots.size();
    }

    /**
     * This method gets outlines of a current page
     *
     * @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 return all outlines of a current page
     */
    public List<PdfOutline> getOutlines(boolean updateOutlines) {
        getDocument().getOutlines(updateOutlines);
        return getDocument().getCatalog().getPagesWithOutlines().get(getPdfObject());
    }

    /**
     * @return true - if in case the page has a rotation, then new content will be automatically rotated in the
     * opposite direction. On the rotated page this would look like if new content ignores page rotation.
     */
    public boolean isIgnorePageRotationForContent() {
        return ignorePageRotationForContent;
    }

    /**
     * If true - defines that in case the page has a rotation, then new content will be automatically rotated in the
     * opposite direction. On the rotated page this would look like if new content ignores page rotation.
     * Default value - {@code false}.
     *
     * @param ignorePageRotationForContent - true to ignore rotation of the new content on the rotated page.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setIgnorePageRotationForContent(boolean ignorePageRotationForContent) {
        this.ignorePageRotationForContent = ignorePageRotationForContent;
        return this;
    }

    /**
     * This method adds or replaces a page label.
     *
     * @param numberingStyle The numbering style that shall be used for the numeric portion of each page label.
     *                       May be NULL
     * @param labelPrefix    The label prefix for page labels in this range. May be NULL
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setPageLabel(PageLabelNumberingStyle numberingStyle, String labelPrefix) {
        return setPageLabel(numberingStyle, labelPrefix, 1);
    }

    /**
     * This method adds or replaces a page label.
     *
     * @param numberingStyle The numbering style that shall be used for the numeric portion of each page label.
     *                       May be NULL
     * @param labelPrefix    The label prefix for page labels in this range. May be NULL
     * @param firstPage      The value of the numeric portion for the first page label in the range. Must be greater or
     *                       equal 1.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setPageLabel(PageLabelNumberingStyle numberingStyle, String labelPrefix, int firstPage) {
        if (firstPage < 1)
            throw new PdfException(
                    KernelExceptionMessageConstant.IN_A_PAGE_LABEL_THE_PAGE_NUMBERS_MUST_BE_GREATER_OR_EQUAL_TO_1);
        PdfDictionary pageLabel = new PdfDictionary();
        if (numberingStyle != null) {
            switch (numberingStyle) {
                case DECIMAL_ARABIC_NUMERALS:
                    pageLabel.put(PdfName.S, PdfName.D);
                    break;
                case UPPERCASE_ROMAN_NUMERALS:
                    pageLabel.put(PdfName.S, PdfName.R);
                    break;
                case LOWERCASE_ROMAN_NUMERALS:
                    pageLabel.put(PdfName.S, PdfName.r);
                    break;
                case UPPERCASE_LETTERS:
                    pageLabel.put(PdfName.S, PdfName.A);
                    break;
                case LOWERCASE_LETTERS:
                    pageLabel.put(PdfName.S, PdfName.a);
                    break;
                default:
            }
        }
        if (labelPrefix != null) {
            pageLabel.put(PdfName.P, new PdfString(labelPrefix));
        }

        if (firstPage != 1) {
            pageLabel.put(PdfName.St, new PdfNumber(firstPage));
        }
        getDocument().getCatalog().getPageLabelsTree(true).addEntry(getDocument().getPageNumber(this) - 1, pageLabel);
        return this;
    }

    /**
     * Sets a name specifying the tab order that shall be used for annotations on the page.
     * The possible values are {@link PdfName#R} (row order), {@link PdfName#C} (column order), and {@link PdfName#S} (structure order).
     * Beginning with PDF 2.0, the possible values also include {@link PdfName#A} (annotations array order) and {@link PdfName#W} (widget order).
     * See ISO 32000 12.5, "Annotations" for details.
     *
     * @param tabOrder a {@link PdfName} specifying the annotations tab order. See method description for the allowed values.
     * @return this {@link PdfPage} instance.
     */
    public PdfPage setTabOrder(PdfName tabOrder) {
        put(PdfName.Tabs, tabOrder);
        return this;
    }

    /**
     * Gets a name specifying the tab order that shall be used for annotations on the page.
     * The possible values are {@link PdfName#R} (row order), {@link PdfName#C} (column order), and {@link PdfName#S} (structure order).
     * Beginning with PDF 2.0, the possible values also include {@link PdfName#A} (annotations array order) and {@link PdfName#W} (widget order).
     * See ISO 32000 12.5, "Annotations" for details.
     *
     * @return a {@link PdfName} specifying the annotations tab order or null if tab order is not defined.
     */
    public PdfName getTabOrder() {
        return getPdfObject().getAsName(PdfName.Tabs);
    }

    /**
     * Sets a stream object that shall define the page���s thumbnail image. Thumbnail images represent the contents of
     * its pages in miniature form
     *
     * @param thumb the thumbnail image
     * @return this {@link PdfPage} object
     */
    public PdfPage setThumbnailImage(PdfImageXObject thumb) {
        return put(PdfName.Thumb, thumb.getPdfObject());
    }

    /**
     * Sets a stream object that shall define the page���s thumbnail image. Thumbnail images represent the contents of
     * its pages in miniature form
     *
     * @return the thumbnail image, or <code>null</code> if it is not present
     */
    public PdfImageXObject getThumbnailImage() {
        PdfStream thumbStream = getPdfObject().getAsStream(PdfName.Thumb);
        return thumbStream != null ? new PdfImageXObject(thumbStream) : null;
    }

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

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

    /**
     * Helper method that associates specified value with the specified key in the underlying
     * {@link PdfDictionary}. Can be used in method chaining.
     *
     * @param key   the {@link PdfName} key with which the specified value is to be associated
     * @param value the {@link PdfObject} value to be associated with the specified key.
     * @return this {@link PdfPage} object.
     */
    public PdfPage put(PdfName key, PdfObject value) {
        getPdfObject().put(key, value);
        setModified();
        return this;
    }

    /**
     * Helper method that removes the value associated with the specified key
     * from the underlying {@link PdfDictionary}. Can be used in method chaining.
     *
     * @param key the {@link PdfName} key for which associated value is to be removed
     *
     * @return this {@link PdfPage} object
     */
    public PdfPage remove(PdfName key) {
        getPdfObject().remove(key);
        setModified();
        return this;
    }

    /**
     * Adds file associated with PDF page and identifies the relationship between them.
     * <p>
     * Associated files may be used in Pdf/A-3 and Pdf 2.0 documents.
     * The method adds file to array value of the AF key in the page dictionary.
     * If description is provided, it also will add file description to catalog Names tree.
     * <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
     */
    public void addAssociatedFile(String description, PdfFileSpec fs) {
        if (null == ((PdfDictionary) fs.getPdfObject()).get(PdfName.AFRelationship)) {
            Logger logger = LoggerFactory.getLogger(PdfPage.class);
            logger.error(IoLogMessageConstant.ASSOCIATED_FILE_SPEC_SHALL_INCLUDE_AFRELATIONSHIP);
        }
        if (null != description) {
            PdfString key = new PdfString(description);
            getDocument().getCatalog()
                    .addNameToNameTree(key, fs.getPdfObject(), PdfName.EmbeddedFiles);
        }
        PdfArray afArray = getPdfObject().getAsArray(PdfName.AF);
        if (afArray == null) {
            afArray = new PdfArray();
            put(PdfName.AF, afArray);
        }
        afArray.add(fs.getPdfObject());
    }

    /**
     * <p>
     * Adds file associated with PDF page and identifies the relationship between them.
     * <p>
     * Associated files may be used in Pdf/A-3 and Pdf 2.0 documents.
     * The method adds file to array value of the AF key in the page dictionary.
     * <p>
     * For associated files their associated file specification dictionaries shall include the AFRelationship key
     *
     * @param fs file specification dictionary of associated file
     */
    public void addAssociatedFile(PdfFileSpec fs) {
        addAssociatedFile(null, fs);
    }

    /**
     * Returns files associated with PDF page.
     *
     * @param create defines whether AF arrays will be created if it doesn't exist
     * @return associated files array
     */
    public PdfArray getAssociatedFiles(boolean create) {
        PdfArray afArray = getPdfObject().getAsArray(PdfName.AF);
        if (afArray == null && create) {
            afArray = new PdfArray();
            put(PdfName.AF, afArray);
        }
        return afArray;
    }

    void tryFlushPageTags() {
        try {
            if (!getDocument().isClosing) {
                getDocument().getTagStructureContext().flushPageTags(this);
            }
            getDocument().getStructTreeRoot().savePageStructParentIndexIfNeeded(this);
        } catch (Exception e) {
            throw new PdfException(
                    KernelExceptionMessageConstant.TAG_STRUCTURE_FLUSHING_FAILED_IT_MIGHT_BE_CORRUPTED, e);
        }
    }

    void releaseInstanceFields() {
        resources = null;
        parentPages = null;
    }

    /**
     * Checks if page rotation inverse matrix (which rotates content into the opposite direction from page rotation
     * direction in order to give the impression of the not rotated text) is already applied to the page content stream.
     * See {@link #setIgnorePageRotationForContent(boolean)} and {@link PageContentRotationHelper}.
     *
     * @return {@code true} if inverse matrix is already applied, {@code false} otherwise
     */
    boolean isPageRotationInverseMatrixWritten() {
        return pageRotationInverseMatrixWritten;
    }

    /**
     * Specifies that page rotation inverse matrix (which rotates content into the opposite direction from page rotation
     * direction in order to give the impression of the not rotated text) is applied to the page content stream.
     * See {@link #setIgnorePageRotationForContent(boolean)} and {@link PageContentRotationHelper}.
     */
    void setPageRotationInverseMatrixWritten() {
        pageRotationInverseMatrixWritten = true;
    }

    @Override
    protected boolean isWrappedObjectMustBeIndirect() {
        return true;
    }

    private static boolean isAnnotInvisible(PdfAnnotation annotation) {
        PdfNumber f = annotation.getPdfObject().getAsNumber(PdfName.F);
        if (f == null) {
            return false;
        }
        int flags = f.intValue();
        return PdfCheckersUtil.checkFlag(flags, PdfAnnotation.INVISIBLE) ||
                (PdfCheckersUtil.checkFlag(flags, PdfAnnotation.NO_VIEW) &&
                        !PdfCheckersUtil.checkFlag(flags, PdfAnnotation.TOGGLE_NO_VIEW));
    }

    private static boolean isReferenceAllowed(String role) {
        // For these roles, Link is an allowed child, but Reference is not.
        return !StandardRoles.DOCUMENT.equals(role) && !StandardRoles.DOCUMENTFRAGMENT.equals(role) &&
                !StandardRoles.ART.equals(role) && !StandardRoles.SECT.equals(role);
    }

    private void tagAnnotation(PdfAnnotation annotation) {
        boolean tagAdded = false;
        boolean presentInTagStructure = true;
        boolean isUA2 = isPdfUA2Document();
        TagTreePointer tagPointer = getDocument().getTagStructureContext().getAutoTaggingPointer();
        if (isUA2 && isAnnotInvisible(annotation)) {
            if (PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
                if (!StandardRoles.ARTIFACT.equals(tagPointer.getRole())) {
                    tagPointer.addTag(StandardRoles.ARTIFACT);
                    tagAdded = true;
                }
            } else {
                presentInTagStructure = false;
            }
        } else {
            tagAdded = addAnnotationTag(tagPointer, annotation);
        }
        if (presentInTagStructure) {
            PdfPage prevPage = tagPointer.getCurrentPage();
            tagPointer.setPageForTagging(this).addAnnotationTag(annotation);
            if (prevPage != null) {
                tagPointer.setPageForTagging(prevPage);
            }
        }
        if (tagAdded) {
            tagPointer.moveToParent();
        }
    }

    private boolean isPdfUA2Document() {
        PdfUAConformance uaConformance = getDocument().getConformance().getUAConformance();
        if (uaConformance == null) {
            try {
                uaConformance = PdfConformance.getConformance(getDocument().getXmpMetadata()).getUAConformance();
            } catch (XMPException e) {
                return false;
            }
        }
        return PdfUAConformance.PDF_UA_2 == uaConformance;
    }

    private void checkIsoConformanceForAnnotation(PdfAnnotation annotation) {
        getDocument().checkIsoConformance(new PdfAnnotationContext(annotation.getPdfObject()));
        if (annotation instanceof PdfLinkAnnotation) {
            PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
            getDocument().checkIsoConformance(new PdfDestinationAdditionContext(linkAnnotation.getDestinationObject()));
            if (linkAnnotation.getAction() != null && PdfName.GoTo.equals(linkAnnotation.getAction().get(PdfName.S))) {
                // We only care about destinations, whose target lies within this document.
                // That's why GoToR and GoToE are ignored.
                getDocument().checkIsoConformance(
                        new PdfDestinationAdditionContext(new PdfAction(linkAnnotation.getAction())));
            }
        }
    }

    private boolean addAnnotationTag(TagTreePointer tagPointer, PdfAnnotation annotation) {
        if (annotation instanceof PdfLinkAnnotation) {
            // "Link" and "Reference" tags were added starting from PDF 1.4
            if (PdfVersion.PDF_1_3.compareTo(getDocument().getPdfVersion()) < 0) {
                if (StandardRoles.REFERENCE.equals(tagPointer.getRole()) ||
                        StandardRoles.LINK.equals(tagPointer.getRole())) {
                    return false;
                }
                String linkRole = ((PdfLinkAnnotation) annotation).getRoleBasedOnDestination(getDocument());
                if (StandardRoles.REFERENCE.equals(linkRole) && isReferenceAllowed(tagPointer.getRole())) {
                    PdfNamespace currentNamespace = tagPointer.getNamespaceForNewTags();
                    if (PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
                        tagPointer.setNamespaceForNewTags(PdfNamespace.getDefault(getDocument()));
                    }
                    tagPointer.addTag(StandardRoles.REFERENCE);
                    tagPointer.setNamespaceForNewTags(currentNamespace);
                    return true;
                }
                tagPointer.addTag(StandardRoles.LINK);
                return true;
            }
        } else {
            if (!(annotation instanceof PdfWidgetAnnotation)
                    && !(annotation instanceof PdfPrinterMarkAnnotation)) {
                // "Annot" tag was added starting from PDF 1.5
                if (PdfVersion.PDF_1_4.compareTo(getDocument().getPdfVersion()) < 0) {
                    if (!StandardRoles.ANNOT.equals(tagPointer.getRole())) {
                        tagPointer.addTag(StandardRoles.ANNOT);
                        return true;
                    }
                }
            }
            if (annotation instanceof PdfPrinterMarkAnnotation &&
                    PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
                if (!StandardRoles.ARTIFACT.equals(tagPointer.getRole())) {
                    tagPointer.addTag(StandardRoles.ARTIFACT);
                    return true;
                }
            }
        }
        return false;
    }

    private PdfPage copyTo(PdfPage page, PdfDocument toDocument, IPdfPageExtraCopier copier) {
        final ICopyFilter copyFilter = new DestinationResolverCopyFilter(this.getDocument(), toDocument);
        copyInheritedProperties(page, toDocument, NullCopyFilter.getInstance());
        copyAnnotations(toDocument, page, copyFilter);

        if (copier != null) {
            copier.copy(this, page);
        } else {
            if (!toDocument.getWriter().isUserWarnedAboutAcroFormCopying && getDocument().hasAcroForm()) {
                Logger logger = LoggerFactory.getLogger(PdfPage.class);
                logger.warn(IoLogMessageConstant.SOURCE_DOCUMENT_HAS_ACROFORM_DICTIONARY);
                toDocument.getWriter().isUserWarnedAboutAcroFormCopying = true;
            }
        }
        return page;
    }

    private PdfArray getAnnots(boolean create) {
        PdfArray annots = getPdfObject().getAsArray(PdfName.Annots);
        if (annots == null && create) {
            annots = new PdfArray();
            put(PdfName.Annots, annots);
        }
        return annots;
    }

    private PdfObject getInheritedValue(PdfName pdfName, int type) {
        if (this.parentPages == null) {
            this.parentPages = getDocument().getCatalog().getPageTree().findPageParent(this);
        }
        PdfObject val = getInheritedValue(this.parentPages, pdfName);
        return val != null && val.getType() == type ? val : null;
    }

    private static PdfObject getInheritedValue(PdfPages parentPages, PdfName pdfName) {
        if (parentPages != null) {
            PdfDictionary parentDictionary = parentPages.getPdfObject();
            PdfObject value = parentDictionary.get(pdfName);
            if (value != null) {
                return value;
            } else {
                return getInheritedValue(parentPages.getParent(), pdfName);
            }
        }
        return null;
    }

    private PdfStream newContentStream(boolean before) {
        PdfObject contents = getPdfObject().get(PdfName.Contents);
        PdfArray array;
        if (contents instanceof PdfStream) {
            array = new PdfArray();
            if (contents.getIndirectReference() != null) {
                // Explicitly using object indirect reference here in order to correctly process released objects.
                array.add(contents.getIndirectReference());
            } else {
                array.add(contents);
            }
            put(PdfName.Contents, array);
        } else if (contents instanceof PdfArray) {
            array = (PdfArray) contents;
        } else {
            array = null;
        }
        PdfStream contentStream = (PdfStream) new PdfStream().makeIndirect(getDocument());
        if (array != null) {
            if (before) {
                array.add(0, contentStream);
            } else {
                array.add(contentStream);
            }
            if (array.getIndirectReference() != null) {
                array.setModified();
            } else {
                setModified();
            }
        } else {
            put(PdfName.Contents, contentStream);
        }
        return contentStream;
    }

    private void copyAnnotations(PdfDocument toDocument,PdfPage page, ICopyFilter copyFilter) {
        for (final PdfAnnotation annot : getAnnotations()) {
            if (copyFilter.shouldProcess(page.getPdfObject(), null, annot.getPdfObject())) {
                final PdfAnnotation newAnnot = PdfAnnotation.makeAnnotation(
                        annot.getPdfObject().copyTo(toDocument, Arrays.asList(PdfName.P, PdfName.Parent), true,
                                copyFilter)
                );
                if (PdfName.Widget.equals(annot.getSubtype())) {
                    rebuildFormFieldParent(annot.getPdfObject(), newAnnot.getPdfObject(), toDocument);
                }
                // P will be set in PdfPage#addAnnotation; Parent will be regenerated in PdfPageExtraCopier.
                page.addAnnotation(-1, newAnnot, false);
            }
        }
    }

    private void flushResourcesContentStreams() {
        flushResourcesContentStreams(getResources().getPdfObject());

        PdfArray annots = getAnnots(false);
        if (annots != null && !annots.isFlushed()) {
            for (int i = 0; i < annots.size(); ++i) {
                PdfDictionary apDict = annots.getAsDictionary(i).getAsDictionary(PdfName.AP);
                if (apDict != null) {
                    flushAppearanceStreams(apDict);
                }
            }
        }
    }

    private void flushResourcesContentStreams(PdfDictionary resources) {
        if (resources != null && !resources.isFlushed()) {
            flushWithResources(resources.getAsDictionary(PdfName.XObject));
            flushWithResources(resources.getAsDictionary(PdfName.Pattern));
            flushWithResources(resources.getAsDictionary(PdfName.Shading));
        }
    }

    private void flushWithResources(PdfDictionary objsCollection) {
        if (objsCollection == null || objsCollection.isFlushed()) {
            return;
        }

        for (PdfObject obj : objsCollection.values()) {
            if (obj.isFlushed())
                continue;
            flushResourcesContentStreams(((PdfDictionary) obj).getAsDictionary(PdfName.Resources));
            flushMustBeIndirectObject(obj);
        }
    }

    private void flushAppearanceStreams(PdfDictionary appearanceStreamsDict) {
        if (appearanceStreamsDict.isFlushed()) {
            return;
        }
        for (PdfObject val : appearanceStreamsDict.values()) {
            if (val instanceof PdfDictionary) {
                PdfDictionary ap = (PdfDictionary) val;
                if (ap.isDictionary()) {
                    flushAppearanceStreams(ap);
                } else if (ap.isStream()) {
                    flushMustBeIndirectObject(ap);
                }
            }
        }
    }

    private void flushMustBeIndirectObject(PdfObject obj) {
        // TODO DEVSIX-744
        obj.makeIndirect(getDocument()).flush();
    }

    private void copyInheritedProperties(PdfPage copyPdfPage, PdfDocument pdfDocument, ICopyFilter copyFilter) {
        if (copyPdfPage.getPdfObject().get(PdfName.Resources) == null) {
            final PdfObject copyResource = pdfDocument.getWriter().copyObject(getResources().getPdfObject(),
                    pdfDocument, false, copyFilter);
            copyPdfPage.getPdfObject().put(PdfName.Resources, copyResource);
        }
        if (copyPdfPage.getPdfObject().get(PdfName.MediaBox) == null) {
            //media box shall be in any case
            copyPdfPage.setMediaBox(getMediaBox());
        }
        if (copyPdfPage.getPdfObject().get(PdfName.CropBox) == null) {
            //original pdfObject don't have CropBox, otherwise copyPdfPage will contain it
            PdfArray cropBox = (PdfArray) getInheritedValue(PdfName.CropBox, PdfObject.ARRAY);
            //crop box is optional, we shall not set default value.
            if (cropBox != null) {
                copyPdfPage.put(PdfName.CropBox, cropBox.copyTo(pdfDocument));
            }
        }
        if (copyPdfPage.getPdfObject().get(PdfName.Rotate) == null) {
            //original pdfObject don't have Rotate, otherwise copyPdfPage will contain it
            PdfNumber rotate = (PdfNumber) getInheritedValue(PdfName.Rotate, PdfObject.NUMBER);
            //rotate is optional, we shall not set default value.
            if (rotate != null) {
                copyPdfPage.put(PdfName.Rotate, rotate.copyTo(pdfDocument));
            }
        }
    }

    private void rebuildFormFieldParent(PdfDictionary field, PdfDictionary newField, PdfDocument toDocument) {
        rebuildFormFieldParent(field, newField, toDocument, new HashSet<>());
    }

    private void rebuildFormFieldParent(PdfDictionary field, PdfDictionary newField, PdfDocument toDocument, Set<PdfDictionary> visitedForms) {
        if (newField.containsKey(PdfName.Parent)) {
            return;
        }
        visitedForms.add(field);
        PdfDictionary oldParent = field.getAsDictionary(PdfName.Parent);
        if (oldParent != null) {
            PdfDictionary newParent = oldParent.copyTo(toDocument, Arrays.asList(PdfName.P, PdfName.Kids,
                    PdfName.Parent), false, NullCopyFilter.getInstance());
            if (newParent.isFlushed()) {
                newParent = oldParent.copyTo(toDocument, Arrays.asList(PdfName.P, PdfName.Kids, PdfName.Parent),
                        true, NullCopyFilter.getInstance());
            }
            if (visitedForms.contains(oldParent)) {
                return;
            }
            rebuildFormFieldParent(oldParent, newParent, toDocument, visitedForms);

            PdfArray kids = newParent.getAsArray(PdfName.Kids);
            if (kids == null) {
                // no kids are added here, since we do not know at this point which pages are to be copied,
                // hence we do not know which annotations we should copy
                newParent.put(PdfName.Kids, new PdfArray());
            }
            newField.put(PdfName.Parent, newParent);
        }
    }
}