PdfPageTest.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.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.annot.PdfPopupAnnotation;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

@Tag("UnitTest")
public class PdfPageTest extends ExtendedITextTest {

    @Test
    public void pageConstructorModifiedStateTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        simulateIndirectState(pageDictionary);
        PdfPage pdfPage = new PdfPage(pageDictionary);

        Assertions.assertFalse(pageDictionary.isModified());
    }

    @Test
    public void removeLastAnnotationTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        simulateIndirectState(pageDictionary);
        PdfDictionary annotDictionary = new PdfDictionary();
        pageDictionary.put(PdfName.Annots, new PdfArray(Collections.singletonList((PdfObject) annotDictionary)));

        Assertions.assertFalse(pageDictionary.isModified());

        PdfPage pdfPage = new PdfPage(pageDictionary);
        pdfPage.removeAnnotation(PdfAnnotation.makeAnnotation(annotDictionary));

        Assertions.assertTrue(pdfPage.getAnnotations().isEmpty());
        Assertions.assertFalse(pageDictionary.containsKey(PdfName.Annots));
        Assertions.assertTrue(pageDictionary.isModified());
    }

    @Test
    public void removeAnnotationTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        simulateIndirectState(pageDictionary);
        PdfDictionary annotDictionary1 = new PdfDictionary();
        PdfDictionary annotDictionary2 = new PdfDictionary();
        pageDictionary.put(PdfName.Annots, new PdfArray(
                Arrays.asList(annotDictionary1, (PdfObject) annotDictionary2))
        );

        Assertions.assertFalse(pageDictionary.isModified());

        PdfPage pdfPage = new PdfPage(pageDictionary);
        pdfPage.removeAnnotation(PdfAnnotation.makeAnnotation(annotDictionary1));

        Assertions.assertEquals(1, pdfPage.getAnnotations().size());
        Assertions.assertEquals(annotDictionary2, pdfPage.getAnnotations().get(0).getPdfObject());
        Assertions.assertTrue(pageDictionary.isModified());
    }

    @Test
    public void removeAnnotationWithIndirectAnnotsArrayTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        simulateIndirectState(pageDictionary);
        PdfDictionary annotDictionary1 = new PdfDictionary();
        PdfDictionary annotDictionary2 = new PdfDictionary();
        PdfArray annotsArray = new PdfArray(
                Arrays.asList(annotDictionary1, (PdfObject) annotDictionary2)
        );
        simulateIndirectState(annotsArray);

        pageDictionary.put(PdfName.Annots, annotsArray);

        Assertions.assertFalse(annotsArray.isModified());

        PdfPage pdfPage = new PdfPage(pageDictionary);
        pdfPage.removeAnnotation(PdfAnnotation.makeAnnotation(annotDictionary1));

        Assertions.assertEquals(1, pdfPage.getAnnotations().size());
        Assertions.assertEquals(annotDictionary2, pdfPage.getAnnotations().get(0).getPdfObject());
        Assertions.assertFalse(pageDictionary.isModified());
        Assertions.assertTrue(annotsArray.isModified());
    }

    @Test
    public void setArtBoxTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        PdfArray preExistingTrimBoxArr = new PdfArray(new int[] {0, 0, 100, 100});
        pageDictionary.put(PdfName.TrimBox, preExistingTrimBoxArr);
        simulateIndirectState(pageDictionary);

        Assertions.assertFalse(pageDictionary.isModified());

        PdfPage pdfPage = new PdfPage(pageDictionary);
        pdfPage.setArtBox(new Rectangle(25, 40));

        PdfArray expectedArtBoxArr = new PdfArray(
                Arrays.asList(new PdfNumber(0), new PdfNumber(0), new PdfNumber(25), (PdfObject) new PdfNumber(40))
        );
        Assertions.assertTrue(
                new CompareTool().compareArrays(pageDictionary.getAsArray(PdfName.ArtBox), expectedArtBoxArr));
        // trimbox not removed
        Assertions.assertTrue(
                new CompareTool().compareArrays(pageDictionary.getAsArray(PdfName.TrimBox), preExistingTrimBoxArr));
        Assertions.assertTrue(pageDictionary.isModified());
    }

    @Test
    public void setTrimBoxTest() {
        PdfDictionary pageDictionary = new PdfDictionary();
        PdfArray preExistingArtBoxArr = new PdfArray(new int[] {0, 0, 100, 100});
        pageDictionary.put(PdfName.ArtBox, preExistingArtBoxArr);
        simulateIndirectState(pageDictionary);

        Assertions.assertFalse(pageDictionary.isModified());

        PdfPage pdfPage = new PdfPage(pageDictionary);
        pdfPage.setTrimBox(new Rectangle(25, 40));

        PdfArray expectedTrimBoxArr = new PdfArray(
                Arrays.asList(new PdfNumber(0), new PdfNumber(0), new PdfNumber(25), (PdfObject) new PdfNumber(40))
        );
        Assertions.assertTrue(
                new CompareTool().compareArrays(pageDictionary.getAsArray(PdfName.TrimBox), expectedTrimBoxArr));
        // artbox not removed
        Assertions.assertTrue(
                new CompareTool().compareArrays(pageDictionary.getAsArray(PdfName.ArtBox), preExistingArtBoxArr));
        Assertions.assertTrue(pageDictionary.isModified());
    }

    @Test
    public void addAnnotationAnnotTagPDF2Test() throws IOException {
        byte[] docBytes;
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            try (PdfDocument doc = new PdfDocument(new PdfWriter(outputStream,
                    new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)))) {
                doc.setTagged();
                PdfPage page = new PdfPage(doc);
                PdfPopupAnnotation annot = new PdfPopupAnnotation(new Rectangle(100, 100, 100, 100));
                annot.setName(new PdfString("this is a pop up"));
                page.addAnnotation(annot);
            }
            docBytes = outputStream.toByteArray();
        }

        try (PdfDocument docReopen = new PdfDocument(new PdfReader(new ByteArrayInputStream(docBytes)))) {
            PdfDictionary structTreeRoot = docReopen.getCatalog().getPdfObject().getAsDictionary(PdfName.StructTreeRoot);
            PdfDictionary structElem = ((PdfDictionary) structTreeRoot.getAsArray(PdfName.K).get(0)).
                    getAsDictionary(PdfName.K);
            Assertions.assertEquals(PdfName.Annot, structElem.getAsName(PdfName.S));
            Assertions.assertEquals(new PdfString("this is a pop up"), structElem.getAsDictionary(PdfName.K).
                    getAsDictionary(PdfName.Obj).getAsString(PdfName.NM));
        }
    }

    /**
     * Simulates indirect state of object making sure it is not marked as modified.
     *
     * @param obj object to which indirect state simulation is applied
     */
    private void simulateIndirectState(PdfObject obj) {
        try (PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            dummyDoc.addNewPage();
            obj.setIndirectReference(new PdfIndirectReference(dummyDoc, 0));
        }
    }
}