PdfDocumentTest.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.FileUtil;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.DeflaterOutputStream;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.PdfReader.StrictnessLevel;
import com.itextpdf.kernel.pdf.annot.PdfTextAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.kernel.pdf.navigation.PdfDestination;
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.kernel.xmp.options.SerializeOptions;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.LogLevelConstants;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("BouncyCastleIntegrationTest")
public class PdfDocumentTest extends ExtendedITextTest {

    public static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/pdf/PdfDocumentTest/";
    public static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/pdf/PdfDocumentTest/";

    @BeforeAll
    public static void beforeClass() {
        createOrClearDestinationFolder(DESTINATION_FOLDER);
    }

    @AfterAll
    public static void afterClass() {
        CompareTool.cleanup(DESTINATION_FOLDER);
    }

    @Test
    public void missingProducerTest() throws IOException {
        String inputFile = SOURCE_FOLDER + "missingProducer.pdf";
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try (PdfDocument document = new PdfDocument(new PdfReader(inputFile), new PdfWriter(outputStream))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertNull(documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNull(documentInfo.getProducer());
        }

        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());

        try (PdfDocument document = new PdfDocument(new PdfReader(inputStream),
                new PdfWriter(new ByteArrayOutputStream()))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertNotNull(documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNotNull(document.getDocumentInfo().getProducer());
        }
    }

    @Test
    public void nullProducerTest() throws IOException {
        String inputFile = SOURCE_FOLDER + "nullProducer.pdf";
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try (PdfDocument document = new PdfDocument(new PdfReader(inputFile), new PdfWriter(outputStream))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertEquals(PdfNull.PDF_NULL, documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNull(documentInfo.getProducer());
        }

        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());

        try (PdfDocument document = new PdfDocument(new PdfReader(inputStream), new PdfWriter(new ByteArrayOutputStream()))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertNotNull(documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNotNull(document.getDocumentInfo().getProducer());
        }
    }

    @Test
    public void nameProducerTest() throws IOException {
        String inputFile = SOURCE_FOLDER + "nameProducer.pdf";
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        try (PdfDocument document = new PdfDocument(new PdfReader(inputFile), new PdfWriter(outputStream))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertEquals(new PdfName("producerAsName"), documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNull(documentInfo.getProducer());
        }

        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());

        try (PdfDocument document = new PdfDocument(new PdfReader(inputStream), new PdfWriter(new ByteArrayOutputStream()))) {
            PdfDocumentInfo documentInfo = document.getDocumentInfo();
            Assertions.assertNotNull(documentInfo.getPdfObject().get(PdfName.Producer));
            Assertions.assertNotNull(document.getDocumentInfo().getProducer());
        }
    }

    @Test
    public void writingVersionTest01() throws IOException {
        // There is a possibility to override version in stamping mode
        String out = DESTINATION_FOLDER + "writing_pdf_version.pdf";

        PdfDocument pdfDoc = new PdfDocument(
                CompareTool.createTestPdfWriter(out, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)));

        Assertions.assertEquals(PdfVersion.PDF_2_0, pdfDoc.getPdfVersion());

        pdfDoc.addNewPage();
        pdfDoc.close();

        PdfDocument assertPdfDoc = new PdfDocument(CompareTool.createOutputReader(out));
        Assertions.assertEquals(PdfVersion.PDF_2_0, assertPdfDoc.getPdfVersion());
        assertPdfDoc.close();
    }

    //We have this test in PdfOutlineTest as well, because we had some issues with outlines before. One test worked
    // fine, while another one failed.
    @Test
    public void addOutlinesWithNamedDestinations01() throws IOException, InterruptedException {
        String filename = DESTINATION_FOLDER + "outlinesWithNamedDestinations01.pdf";

        PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "iphone_user_guide.pdf"),
                CompareTool.createTestPdfWriter(filename));
        PdfArray array1 = new PdfArray();
        array1.add(pdfDoc.getPage(2).getPdfObject());
        array1.add(PdfName.XYZ);
        array1.add(new PdfNumber(36));
        array1.add(new PdfNumber(806));
        array1.add(new PdfNumber(0));

        PdfArray array2 = new PdfArray();
        array2.add(pdfDoc.getPage(3).getPdfObject());
        array2.add(PdfName.XYZ);
        array2.add(new PdfNumber(36));
        array2.add(new PdfNumber(806));
        array2.add(new PdfNumber(1.25));

        PdfArray array3 = new PdfArray();
        array3.add(pdfDoc.getPage(4).getPdfObject());
        array3.add(PdfName.XYZ);
        array3.add(new PdfNumber(36));
        array3.add(new PdfNumber(806));
        array3.add(new PdfNumber(1));

        pdfDoc.addNamedDestination("test1", array2);
        pdfDoc.addNamedDestination("test2", array3);
        pdfDoc.addNamedDestination("test3", array1);

        PdfOutline root = pdfDoc.getOutlines(false);

        PdfOutline firstOutline = root.addOutline("Test1");
        firstOutline.addDestination(PdfDestination.makeDestination(new PdfString("test1")));
        PdfOutline secondOutline = root.addOutline("Test2");
        secondOutline.addDestination(PdfDestination.makeDestination(new PdfString("test2")));
        PdfOutline thirdOutline = root.addOutline("Test3");
        thirdOutline.addDestination(PdfDestination.makeDestination(new PdfString("test3")));
        pdfDoc.close();

        Assertions.assertNull(new CompareTool()
                .compareByContent(filename, SOURCE_FOLDER + "cmp_outlinesWithNamedDestinations01.pdf",
                        DESTINATION_FOLDER,
                        "diff_"));
    }

    @Test
    public void freeReferencesInObjectStream() throws IOException {
        PdfReader reader = new PdfReader(SOURCE_FOLDER + "styledLineArts_Redacted.pdf");
        PdfWriter writer = new PdfWriter(new ByteArrayOutputStream());
        PdfDocument document = new PdfDocument(reader, writer, new StampingProperties().useAppendMode());
        PdfDictionary dict = new PdfDictionary();
        dict.makeIndirect(document);
        Assertions.assertTrue(dict.getIndirectReference().getObjNumber() > 0);
    }

    @Test
    public void removeUnusedObjectsInWriterModeTest() throws IOException {
        String filename = "removeUnusedObjectsInWriter.pdf";

        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filename));

        pdfDocument.addNewPage();

        PdfDictionary unusedDictionary = new PdfDictionary();
        PdfArray unusedArray = (PdfArray) new PdfArray().makeIndirect(pdfDocument);
        unusedArray.add(new PdfNumber(42));
        unusedDictionary.put(new PdfName("testName"), unusedArray);

        unusedDictionary.makeIndirect(pdfDocument);

        Assertions.assertEquals(pdfDocument.getXref().size(), 8);
        //on closing, all unused objects shall not be written to resultant document
        pdfDocument.close();

        PdfDocument testerDocument = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filename));
        Assertions.assertEquals(testerDocument.getXref().size(), 6);
        testerDocument.close();
    }

    @Test
    public void removeUnusedObjectsInStampingModeTest() throws IOException {
        String filenameIn = "docWithUnusedObjects_1.pdf";
        String filenameOut = "removeUnusedObjectsInStamping.pdf";

        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filenameIn));

        pdfDocument.addNewPage();

        PdfDictionary unusedDictionary = new PdfDictionary();
        PdfArray unusedArray = (PdfArray) new PdfArray().makeIndirect(pdfDocument);
        unusedArray.add(new PdfNumber(42));
        unusedDictionary.put(new PdfName("testName"), unusedArray);

        unusedDictionary.makeIndirect(pdfDocument).flush();
        pdfDocument.close();

        PdfDocument doc = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filenameIn),
                CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filenameOut));
        Assertions.assertEquals(doc.getXref().size(), 8);
        //on closing, all unused objects shall not be written to resultant document
        doc.close();

        PdfDocument testerDocument = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filenameOut));
        Assertions.assertEquals(testerDocument.getXref().size(), 6);
        testerDocument.close();
    }


    @Test
    public void addUnusedObjectsInWriterModeTest() throws IOException {
        String filename = "addUnusedObjectsInWriter.pdf";

        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filename));

        pdfDocument.addNewPage();

        PdfDictionary unusedDictionary = new PdfDictionary();
        PdfArray unusedArray = (PdfArray) new PdfArray().makeIndirect(pdfDocument);
        unusedArray.add(new PdfNumber(42));
        unusedDictionary.put(new PdfName("testName"), unusedArray);

        unusedDictionary.makeIndirect(pdfDocument);

        Assertions.assertEquals(pdfDocument.getXref().size(), 8);
        pdfDocument.setFlushUnusedObjects(true);
        pdfDocument.close();

        PdfDocument testerDocument = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filename));
        Assertions.assertEquals(testerDocument.getXref().size(), 8);
        testerDocument.close();
    }

    @Test
    public void addUnusedObjectsInStampingModeTest() throws IOException {
        String filenameIn = "docWithUnusedObjects_2.pdf";
        String filenameOut = "addUnusedObjectsInStamping.pdf";

        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filenameIn));

        pdfDocument.addNewPage();

        PdfDictionary unusedDictionary = new PdfDictionary();
        PdfArray unusedArray = (PdfArray) new PdfArray().makeIndirect(pdfDocument);
        unusedArray.add(new PdfNumber(42));
        unusedDictionary.put(new PdfName("testName"), unusedArray);

        unusedDictionary.makeIndirect(pdfDocument).flush();
        pdfDocument.close();

        PdfDocument doc = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filenameIn),
                CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filenameOut));
        Assertions.assertEquals(doc.getXref().size(), 8);
        doc.setFlushUnusedObjects(true);
        doc.close();

        PdfDocument testerDocument = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filenameOut));
        Assertions.assertEquals(testerDocument.getXref().size(), 8);
        testerDocument.close();
    }

    @Test
    public void addUnusedStreamObjectsTest() throws IOException {
        String filenameIn = "docWithUnusedObjects_3.pdf";

        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + filenameIn));

        pdfDocument.addNewPage();

        PdfDictionary unusedDictionary = new PdfDictionary();
        PdfArray unusedArray = (PdfArray) new PdfArray().makeIndirect(pdfDocument);
        unusedArray.add(new PdfNumber(42));
        PdfStream stream = new PdfStream(new byte[] {1, 2, 34, 45}, 0);
        unusedArray.add(stream);
        unusedDictionary.put(new PdfName("testName"), unusedArray);
        unusedDictionary.makeIndirect(pdfDocument).flush();
        pdfDocument.setFlushUnusedObjects(true);
        pdfDocument.close();

        PdfDocument testerDocument = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + filenameIn));
        Assertions.assertEquals(testerDocument.getXref().size(), 9);
        testerDocument.close();
    }

    @Test
    public void testImageCompressLevel() throws IOException {
        byte[] b = ImageDataFactory.create(SOURCE_FOLDER + "berlin2013.jpg").getData();
        com.itextpdf.io.source.ByteArrayOutputStream image = new com.itextpdf.io.source.ByteArrayOutputStream();
        image.assignBytes(b, b.length);

        ByteArrayOutputStream byteArrayStream1 = new com.itextpdf.io.source.ByteArrayOutputStream();
        DeflaterOutputStream zip = new DeflaterOutputStream(byteArrayStream1, 9);
        image.writeTo(zip);

        ByteArrayOutputStream byteArrayStream2 = new com.itextpdf.io.source.ByteArrayOutputStream();
        DeflaterOutputStream zip2 = new DeflaterOutputStream(byteArrayStream2, -1);
        image.writeTo(zip2);

        Assertions.assertTrue(byteArrayStream1.size() == byteArrayStream2.size());
        zip.close();
        zip2.close();
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = IoLogMessageConstant.FLUSHED_OBJECT_CONTAINS_FREE_REFERENCE))
    public void testFreeReference() throws IOException, InterruptedException {
        PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + "freeReference.pdf",
                new WriterProperties().setFullCompressionMode(false));
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "baseFreeReference.pdf"), writer);
        pdfDocument.getPage(1).getResources().getPdfObject().getAsArray(new PdfName("d")).get(0).getIndirectReference()
                .setFree();
        PdfStream pdfStream = new PdfStream();
        pdfStream.setData(new byte[] {24, 23, 67});
        pdfStream.makeIndirect(pdfDocument);
        pdfDocument.getPage(1).getResources().getPdfObject().getAsArray(new PdfName("d")).add(pdfStream);
        pdfDocument.close();
        Assertions.assertNull(new CompareTool()
                .compareByContent(DESTINATION_FOLDER + "freeReference.pdf", SOURCE_FOLDER + "cmp_freeReference.pdf",
                        DESTINATION_FOLDER, "diff_"));
    }

    @Test
    public void fullCompressionAppendMode() throws IOException, InterruptedException {
        PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + "fullCompressionAppendMode.pdf",
                new WriterProperties()
                        .setFullCompressionMode(true)
                        .setCompressionLevel(CompressionConstants.NO_COMPRESSION));
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "fullCompressionDoc.pdf"), writer,
                new StampingProperties().useAppendMode());

        PdfPage page = pdfDocument.getPage(1);
        PdfStream contentStream = new PdfStream();
        String contentStr = new String(pdfDocument.getPage(1).getFirstContentStream().getBytes(),
                StandardCharsets.US_ASCII);
        contentStream.setData(contentStr.replace("/F1 16", "/F1 24").getBytes(StandardCharsets.US_ASCII));
        page.getPdfObject().put(PdfName.Contents, contentStream);
        page.setModified();

        pdfDocument.close();

        Assertions.assertNull(new CompareTool().compareByContent(DESTINATION_FOLDER + "fullCompressionAppendMode.pdf",
                SOURCE_FOLDER + "cmp_fullCompressionAppendMode.pdf", DESTINATION_FOLDER, "diff_"));

        PdfDocument assertDoc = new PdfDocument(CompareTool.createOutputReader(DESTINATION_FOLDER + "fullCompressionAppendMode.pdf"));
        Assertions.assertTrue(assertDoc.getPdfObject(9).isStream());
        Assertions.assertEquals(1, ((PdfDictionary) assertDoc.getPdfObject(9)).getAsNumber(PdfName.N).intValue());
    }

    @Test
    public void checkAndResolveCircularReferences() throws IOException, InterruptedException {
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "datasheet.pdf"),
                CompareTool.createTestPdfWriter(DESTINATION_FOLDER + "datasheet_mode.pdf"));
        PdfDictionary pdfObject = (PdfDictionary) pdfDocument.getPdfObject(53);
        pdfDocument.getPage(1).getResources().addForm((PdfStream) pdfObject);
        pdfDocument.close();
        Assertions.assertNull(new CompareTool()
                .compareByContent(DESTINATION_FOLDER + "datasheet_mode.pdf", SOURCE_FOLDER + "cmp_datasheet_mode.pdf",
                        DESTINATION_FOLDER, "diff_"));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT, 
            ignore = true))
    public void readEncryptedDocumentWithFullCompression() throws IOException {
        PdfReader reader = new PdfReader(SOURCE_FOLDER + "source.pdf",
                new ReaderProperties().setPassword("123".getBytes()));
        PdfDocument pdfDocument = new PdfDocument(reader);

        PdfDictionary form = pdfDocument.getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm);

        PdfDictionary field = form.getAsArray(PdfName.Fields).getAsDictionary(0);

        Assertions.assertEquals("ch", field.getAsString(PdfName.T).toUnicodeString());
        Assertions.assertEquals("SomeStringValueInDictionary",
                field.getAsDictionary(new PdfName("TestDic")).getAsString(new PdfName("TestString")).toUnicodeString());
        Assertions.assertEquals("SomeStringValueInArray",
                field.getAsArray(new PdfName("TestArray")).getAsString(0).toUnicodeString());
        pdfDocument.close();
    }

    @Test
    public void addAssociatedFilesTest01() throws IOException, InterruptedException {
        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + "add_associated_files01.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)));
        pdfDocument.setTagged();
        pdfDocument.addAssociatedFile("af_1", PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 1".getBytes(), "af_1.txt", PdfName.Data));
        pdfDocument.addNewPage();
        pdfDocument.getFirstPage().addAssociatedFile("af_2", PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 2".getBytes(), "af_2.txt", PdfName.Data));

        PdfStructTreeRoot root = pdfDocument.getStructTreeRoot();
        root.addAssociatedFile("af_3", PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 3".getBytes(), "af_3.txt", PdfName.Data));

        PdfFileSpec af5 = PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 5".getBytes(), "af_5", "af_5.txt", PdfName.Data);
        PdfTextAnnotation textannot = new PdfTextAnnotation(new Rectangle(100, 600, 50, 40));
        textannot.setText(new PdfString("Text Annotation 01")).setContents(new PdfString("Some contents..."));
        textannot.addAssociatedFile(af5);
        pdfDocument.getFirstPage().addAnnotation(textannot);

        pdfDocument.close();
        Assertions.assertNull(new CompareTool().compareByContent(DESTINATION_FOLDER + "add_associated_files01.pdf",
                SOURCE_FOLDER + "cmp_add_associated_files01.pdf", DESTINATION_FOLDER, "diff_"));
    }

    @Test
    public void addAssociatedFilesTest02() throws IOException, InterruptedException {
        PdfDocument pdfDocument = new PdfDocument(CompareTool.createTestPdfWriter(DESTINATION_FOLDER + "add_associated_files02.pdf",
                new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)));
        pdfDocument.setTagged();
        PdfCanvas pageCanvas = new PdfCanvas(pdfDocument.addNewPage());

        PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.create(SOURCE_FOLDER + "berlin2013.jpg"));
        imageXObject.addAssociatedFile(PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 1".getBytes(), "af_1.txt", PdfName.Data));

        pageCanvas.addXObjectAt(imageXObject, 40, 400);

        PdfFormXObject formXObject = new PdfFormXObject(new Rectangle(200, 200));
        PdfCanvas formCanvas = new PdfCanvas(formXObject, pdfDocument);
        formCanvas
                .saveState()
                .circle(100, 100, 50)
                .setColor(ColorConstants.BLACK, true)
                .fill()
                .restoreState();
        formCanvas.release();
        formXObject.addAssociatedFile(PdfFileSpec
                .createEmbeddedFileSpec(pdfDocument, "Associated File 2".getBytes(), "af_2.txt", PdfName.Data));

        pageCanvas.addXObjectAt(formXObject, 40, 100);

        pdfDocument.close();
        Assertions.assertNull(new CompareTool().compareByContent(DESTINATION_FOLDER + "add_associated_files02.pdf",
                SOURCE_FOLDER + "cmp_add_associated_files02.pdf", DESTINATION_FOLDER, "diff_"));
    }

    @Test
    public void ignoreTagStructureTest() throws IOException {
        String srcFile = SOURCE_FOLDER + "ignoreTagStructureTest.pdf";
        PdfDocument doNotIgnoreTagStructureDocument = new PdfDocument(new PdfReader(srcFile));
        IgnoreTagStructurePdfDocument ignoreTagStructureDocument = new IgnoreTagStructurePdfDocument(
                new PdfReader(srcFile));
        Assertions.assertTrue(doNotIgnoreTagStructureDocument.isTagged());
        Assertions.assertFalse(ignoreTagStructureDocument.isTagged());

        doNotIgnoreTagStructureDocument.close();
        ignoreTagStructureDocument.close();
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.OUTLINE_DESTINATION_PAGE_NUMBER_IS_OUT_OF_BOUNDS, logLevel = LogLevelConstants.WARN)
    })
    public void removePageWithInvalidOutlineTest() throws IOException, InterruptedException {
        String source = SOURCE_FOLDER + "invalid_outline.pdf";
        String destination = DESTINATION_FOLDER + "invalid_outline.pdf";
        String cmp = SOURCE_FOLDER + "cmp_invalid_outline.pdf";

        PdfDocument document = new PdfDocument(new PdfReader(FileUtil.getInputStreamForFile(source)),
                CompareTool.createTestPdfWriter(destination));

        document.removePage(4);
        document.close();

        Assertions.assertNull(new CompareTool().compareByContent(destination, cmp, DESTINATION_FOLDER, "diff_"));
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.DOCUMENT_VERSION_IN_CATALOG_CORRUPTED,
                    logLevel = LogLevelConstants.ERROR)
    })
    public void openDocumentWithInvalidCatalogVersionTest() throws IOException {
        try (PdfReader reader = new PdfReader(SOURCE_FOLDER + "sample-with-invalid-catalog-version.pdf");
                PdfDocument pdfDocument = new PdfDocument(reader)) {
            Assertions.assertNotNull(pdfDocument);
        }
    }

    @Test
    public void openDocumentWithInvalidCatalogVersionAndConservativeStrictnessReadingTest() throws IOException {
        try (PdfReader reader = new PdfReader(SOURCE_FOLDER + "sample-with-invalid-catalog-version.pdf")
                .setStrictnessLevel(StrictnessLevel.CONSERVATIVE)) {

            Exception e = Assertions.assertThrows(PdfException.class,
                    () -> new PdfDocument(reader)
            );
            Assertions.assertEquals(IoLogMessageConstant.DOCUMENT_VERSION_IN_CATALOG_CORRUPTED, e.getMessage());
        }
    }

    @Test
    public void widgetDaEntryRemovePageTest() throws IOException, InterruptedException {
        final String testName = "widgetDaEntryRemovePage.pdf";
        final String outPdf = DESTINATION_FOLDER + testName;
        try (PdfDocument pdfDocument = new PdfDocument(
                new PdfReader(SOURCE_FOLDER + "widgetWithDaEntry.pdf"), CompareTool.createTestPdfWriter(outPdf))) {
            pdfDocument.removePage(3);
        }
        Assertions.assertNull(new CompareTool().compareByContent(outPdf, SOURCE_FOLDER + "cmp_" + testName,
                DESTINATION_FOLDER));
    }

    @Test
    public void mergedAndSimpleWidgetsRemovePageTest() throws IOException, InterruptedException {
        final String testName = "mergedAndSimpleWidgetsRemovePage.pdf";
        final String outPdf = DESTINATION_FOLDER + testName;
        try (PdfDocument pdfDocument = new PdfDocument(
                new PdfReader(SOURCE_FOLDER + "mergedAndSimpleWidgets.pdf"), CompareTool.createTestPdfWriter(outPdf))) {
            pdfDocument.removePage(1);
        }
        Assertions.assertNull(new CompareTool().compareByContent(outPdf, SOURCE_FOLDER + "cmp_" + testName,
                DESTINATION_FOLDER));
    }

    @Test
    public void mergedSiblingWidgetsRemovePageTest() throws IOException, InterruptedException {
        final String testName = "mergedSiblingWidgetsRemovePage.pdf";
        final String outPdf = DESTINATION_FOLDER + testName;
        try (PdfDocument pdfDocument = new PdfDocument(
                new PdfReader(SOURCE_FOLDER + "mergedSiblingWidgets.pdf"), CompareTool.createTestPdfWriter(outPdf))) {
            pdfDocument.removePage(2);
        }
        Assertions.assertNull(new CompareTool().compareByContent(outPdf, SOURCE_FOLDER + "cmp_" + testName,
                DESTINATION_FOLDER));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate =
            IoLogMessageConstant.XREF_ERROR_WHILE_READING_TABLE_WILL_BE_REBUILT_WITH_CAUSE))
    public void rootCannotBeReferenceFromTrailerTest() throws IOException {
        String filename = SOURCE_FOLDER + "rootCannotBeReferenceFromTrailerTest.pdf";
        PdfReader corruptedReader = new PdfReader(filename);
        Exception e = Assertions.assertThrows(PdfException.class, () -> new PdfDocument(corruptedReader));
        Assertions.assertEquals(KernelExceptionMessageConstant.CORRUPTED_ROOT_ENTRY_IN_TRAILER, e.getMessage());
    }

    @Test
    public void setSerializeOptionsTest() {
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        SerializeOptions options = new SerializeOptions().setUseCanonicalFormat(true);
        document.setSerializeOptions(options);
        Assertions.assertEquals(options, document.getSerializeOptions());
    }

    @Test
    public void getDiContainer() {
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        Assertions.assertNotNull(document.getDiContainer());
    }

    @Test
    public void getDefaultConformanceLevelTest() {
        PdfDocument document = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        Assertions.assertNotNull(document.getConformance());
        Assertions.assertFalse(document.getConformance().isPdfAOrUa());
    }

    @Test
    public void ocgWithTwoParentsTest() throws IOException {
        String inputPdf = "ocgWithTwoParents.pdf";
        PdfDocument doc = new PdfDocument(new PdfReader(SOURCE_FOLDER + inputPdf));
        List<PdfLayer> layerList = doc.getCatalog().getOCProperties(false).getLayers();
        Assertions.assertEquals(2, layerList.get(4).getParents().size());
        doc.close();
    }

    @Test
    public void createPdfDocumentWithAAndUaMetadataTest() throws IOException, InterruptedException {
        String outputPdf = "pdfDocWithAAndUaMetadata.pdf";
        final WriterProperties writerProperties = new WriterProperties().addPdfAXmpMetadata(PdfAConformance.PDF_A_3A)
                .addPdfUaXmpMetadata(PdfUAConformance.PDF_UA_1);
        PdfDocument doc = new PdfDocument(new PdfWriter(DESTINATION_FOLDER + outputPdf, writerProperties));
        doc.addNewPage();
        doc.close();

        Assertions.assertNull(new CompareTool().compareByContent(DESTINATION_FOLDER + outputPdf,
                SOURCE_FOLDER + "cmp_" + outputPdf, DESTINATION_FOLDER));
    }

    private static class IgnoreTagStructurePdfDocument extends PdfDocument {

        IgnoreTagStructurePdfDocument(PdfReader reader) {
            super(reader);
        }

        @Override
        protected void tryInitTagStructure(PdfDictionary str) {
        }
    }
}