PdfDocumentUnitTest.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.font.AdobeGlyphList;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfType3Font;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.kernel.pdf.layer.PdfOCProperties;
import com.itextpdf.kernel.validation.IValidationChecker;
import com.itextpdf.kernel.validation.IValidationContext;
import com.itextpdf.kernel.validation.ValidationContainer;
import com.itextpdf.kernel.validation.ValidationType;
import com.itextpdf.kernel.validation.context.PdfDocumentValidationContext;
import com.itextpdf.test.AssertUtil;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.LogLevelConstants;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("BouncyCastleUnitTest")
public class PdfDocumentUnitTest extends ExtendedITextTest {

    private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/pdf/PdfDocumentUnitTest/";

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.TYPE3_FONT_INITIALIZATION_ISSUE)
    })
    public void getFontWithDirectFontDictionaryTest() {
        PdfDictionary initialFontDict = new PdfDictionary();
        initialFontDict.put(PdfName.Subtype, PdfName.Type3);
        initialFontDict.put(PdfName.FontMatrix, new PdfArray(new float[]{0.001F, 0, 0, 0.001F, 0, 0}));
        initialFontDict.put(PdfName.Widths, new PdfArray());
        PdfDictionary encoding = new PdfDictionary();
        initialFontDict.put(PdfName.Encoding, encoding);
        PdfArray differences = new PdfArray();
        differences.add(new PdfNumber(AdobeGlyphList.nameToUnicode("a")));
        differences.add(new PdfName("a"));
        encoding.put(PdfName.Differences, differences);


        Assertions.assertNull(initialFontDict.getIndirectReference());
        try (PdfDocument doc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            // prevent no pages exception on close
            doc.addNewPage();

            PdfType3Font font1 = (PdfType3Font) doc.getFont(initialFontDict);
            Assertions.assertNotNull(font1);

            // prevent no glyphs for type3 font on close
            font1.addGlyph('a', 0, 0, 0, 0, 0);
        }
    }

    @Test
    public void copyPagesWithOCGDifferentNames() throws IOException {
        List<List<String>> ocgNames = new ArrayList<>();
        List<String> ocgNames1 = new ArrayList<>();
        ocgNames1.add("Name1");
        List<String> ocgNames2 = new ArrayList<>();
        ocgNames2.add("Name2");
        ocgNames.add(ocgNames1);
        ocgNames.add(ocgNames2);
        List<byte[]> sourceDocuments = PdfDocumentUnitTest.initSourceDocuments(ocgNames);

        try (PdfDocument outDocument = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            for (byte[] docBytes : sourceDocuments) {
                try (PdfDocument fromDocument = new PdfDocument(new PdfReader(new ByteArrayInputStream(docBytes)))) {
                    for (int i = 1; i <= fromDocument.getNumberOfPages(); i++) {
                        fromDocument.copyPagesTo(i, i, outDocument);
                    }
                }
            }
            List<String> layerNames = new ArrayList<>();
            layerNames.add("Name1");
            layerNames.add("Name2");
            PdfDocumentUnitTest.assertLayerNames(outDocument, layerNames);
        }
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.DOCUMENT_HAS_CONFLICTING_OCG_NAMES, count = 3),
    })
    public void copyPagesWithOCGSameName() throws IOException {
        List<List<String>> ocgNames = new ArrayList<>();
        List<String> ocgNames1 = new ArrayList<>();
        ocgNames1.add("Name1");
        ocgNames.add(ocgNames1);
        ocgNames.add(ocgNames1);
        ocgNames.add(ocgNames1);
        ocgNames.add(ocgNames1);
        List<byte[]> sourceDocuments = PdfDocumentUnitTest.initSourceDocuments(ocgNames);

        try (PdfDocument outDocument = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            for (byte[] docBytes : sourceDocuments) {
                try (PdfDocument fromDocument = new PdfDocument(new PdfReader(new ByteArrayInputStream(docBytes)))) {
                    for (int i = 1; i <= fromDocument.getNumberOfPages(); i++) {
                        fromDocument.copyPagesTo(i, i, outDocument);
                    }
                }
            }
            List<String> layerNames = new ArrayList<>();
            layerNames.add("Name1");
            layerNames.add("Name1_0");
            layerNames.add("Name1_1");
            layerNames.add("Name1_2");
            PdfDocumentUnitTest.assertLayerNames(outDocument, layerNames);
        }
    }

    @Test
    public void copyPagesWithOCGSameObject() throws IOException {
        byte[] docBytes;
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            try (PdfDocument document = new PdfDocument(new PdfWriter(outputStream))) {
                PdfPage page = document.addNewPage();
                PdfResources pdfResource = page.getResources();
                PdfDictionary ocg = new PdfDictionary();
                ocg.put(PdfName.Type, PdfName.OCG);
                ocg.put(PdfName.Name, new PdfString("name1"));
                ocg.makeIndirect(document);
                pdfResource.addProperties(ocg);
                PdfPage page2 = document.addNewPage();
                PdfResources pdfResource2 = page2.getResources();
                pdfResource2.addProperties(ocg);
                document.getCatalog().getOCProperties(true);
            }
            docBytes = outputStream.toByteArray();
        }

        try (PdfDocument outDocument = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            try (PdfDocument fromDocument = new PdfDocument(new PdfReader(new ByteArrayInputStream(docBytes)))) {
                fromDocument.copyPagesTo(1, fromDocument.getNumberOfPages(), outDocument);
            }

            List<String> layerNames = new ArrayList<>();
            layerNames.add("name1");
            PdfDocumentUnitTest.assertLayerNames(outDocument, layerNames);
        }
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.OCG_COPYING_ERROR, logLevel = LogLevelConstants.ERROR)
    })
    public void copyPagesFlushedResources() throws IOException {
        byte[] docBytes;
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            try (PdfDocument document = new PdfDocument(new PdfWriter(outputStream))) {
                PdfPage page = document.addNewPage();
                PdfResources pdfResource = page.getResources();
                PdfDictionary ocg = new PdfDictionary();
                ocg.put(PdfName.Type, PdfName.OCG);
                ocg.put(PdfName.Name, new PdfString("name1"));
                ocg.makeIndirect(document);
                pdfResource.addProperties(ocg);
                pdfResource.makeIndirect(document);
                PdfPage page2 = document.addNewPage();
                page2.setResources(pdfResource);
                document.getCatalog().getOCProperties(true);
            }
            docBytes = outputStream.toByteArray();
        }

        PdfWriter writer = new PdfWriter(new ByteArrayOutputStream());
        try (PdfDocument outDocument = new PdfDocument(writer)) {
            try (PdfDocument fromDocument = new PdfDocument(new PdfReader(new ByteArrayInputStream(docBytes)))) {
                fromDocument.copyPagesTo(1, 1, outDocument);

                List<String> layerNames = new ArrayList<>();
                layerNames.add("name1");
                PdfDocumentUnitTest.assertLayerNames(outDocument, layerNames);

                outDocument.flushCopiedObjects(fromDocument);
                fromDocument.copyPagesTo(2, 2, outDocument);

                Assertions.assertNotNull(outDocument.getCatalog());
                PdfOCProperties ocProperties = outDocument.getCatalog().getOCProperties(false);
                Assertions.assertNotNull(ocProperties);
                Assertions.assertEquals(1, ocProperties.getLayers().size());
                PdfLayer layer = ocProperties.getLayers().get(0);
                Assertions.assertTrue(layer.getPdfObject().isFlushed());
            }
        }
    }

    @Test
    public void getDocumentInfoAlreadyClosedTest() throws IOException {
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "pdfWithMetadata.pdf"));
        pdfDocument.close();

        Assertions.assertThrows(PdfException.class, () -> pdfDocument.getDocumentInfo());
    }

    @Test
    public void getDocumentInfoInitializationTest() throws IOException {
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "pdfWithMetadata.pdf"));
        Assertions.assertNotNull(pdfDocument.getDocumentInfo());
        pdfDocument.close();
    }

    @Test
    public void getPdfAConformanceLevelInitializationTest() throws IOException {
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "pdfWithMetadata.pdf"));
        Assertions.assertTrue(pdfDocument.reader.getPdfConformance().isPdfAOrUa());
        pdfDocument.close();
    }

    private static void assertLayerNames(PdfDocument outDocument, List<String> layerNames) {
        Assertions.assertNotNull(outDocument.getCatalog());
        PdfOCProperties ocProperties = outDocument.getCatalog().getOCProperties(true);
        Assertions.assertNotNull(ocProperties);
        Assertions.assertEquals(layerNames.size(), ocProperties.getLayers().size());
        for (int i = 0; i < layerNames.size(); i++) {
            PdfLayer layer = ocProperties.getLayers().get(i);
            Assertions.assertNotNull(layer);
            PdfDocumentUnitTest.assertLayerNameEqual(layerNames.get(i), layer);
        }
    }


    private static List<byte[]> initSourceDocuments(List<List<String>> ocgNames) throws IOException {
        List<byte[]> result = new ArrayList<>();
        for(List<String> names: ocgNames) {
            result.add(PdfDocumentUnitTest.initDocument(names));
        }
        return result;
    }

    private static byte[] initDocument(List<String> names) throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            try (PdfDocument document = new PdfDocument(new PdfWriter(outputStream))) {
                PdfPage page = document.addNewPage();
                PdfResources pdfResource = page.getResources();
                for (String name : names) {
                    PdfDictionary ocg = new PdfDictionary();
                    ocg.put(PdfName.Type, PdfName.OCG);
                    ocg.put(PdfName.Name, new PdfString(name));
                    ocg.makeIndirect(document);
                    pdfResource.addProperties(ocg);
                }
                document.getCatalog().getOCProperties(true);
            }
            return outputStream.toByteArray();
        }
    }

    private static void assertLayerNameEqual(String name, PdfLayer layer) {
        PdfDictionary layerDictionary = layer.getPdfObject();
        Assertions.assertNotNull(layerDictionary);
        Assertions.assertNotNull(layerDictionary.get(PdfName.Name));
        String layerNameString = layerDictionary.get(PdfName.Name).toString();
        Assertions.assertEquals(name, layerNameString);
    }

    @Test
    public void cannotGetTagStructureForUntaggedDocumentTest() {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc.getTagStructureContext());
        Assertions.assertEquals(KernelExceptionMessageConstant.MUST_BE_A_TAGGED_DOCUMENT, exception.getMessage());
    }

    @Test
    public void cannotAddPageAfterDocumentIsClosedTest() {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc.addNewPage(1);
        pdfDoc.close();
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc.addNewPage(2));
        Assertions.assertEquals(KernelExceptionMessageConstant.DOCUMENT_CLOSED_IT_IS_IMPOSSIBLE_TO_EXECUTE_ACTION,
                exception.getMessage());
    }

    @Test
    public void cannotMovePageToZeroPositionTest() {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc.addNewPage();
        Exception exception = Assertions.assertThrows(IndexOutOfBoundsException.class,
                () -> pdfDoc.movePage(1, 0));
        Assertions.assertEquals(
                MessageFormatUtil.format(KernelExceptionMessageConstant.REQUESTED_PAGE_NUMBER_IS_OUT_OF_BOUNDS, 0),
                exception.getMessage());
    }

    @Test
    public void cannotMovePageToNegativePosition() {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc.addNewPage();
        Exception exception = Assertions.assertThrows(IndexOutOfBoundsException.class,
                () -> pdfDoc.movePage(1, -1));
        Assertions.assertEquals(
                MessageFormatUtil.format(KernelExceptionMessageConstant.REQUESTED_PAGE_NUMBER_IS_OUT_OF_BOUNDS, -1),
                exception.getMessage());
    }

    @Test
    public void cannotMovePageToOneMorePositionThanPagesNumberTest() {
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc.addNewPage();
        Exception exception = Assertions.assertThrows(IndexOutOfBoundsException.class,
                () -> pdfDoc.movePage(1, 3));
        Assertions.assertEquals(
                MessageFormatUtil.format(KernelExceptionMessageConstant.REQUESTED_PAGE_NUMBER_IS_OUT_OF_BOUNDS, 3),
                exception.getMessage());
    }

    @Test
    public void cannotAddPageToAnotherDocumentTest() {
        PdfDocument pdfDoc1 = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfDocument pdfDoc2 = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc1.addNewPage(1);
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc2.checkAndAddPage(1, pdfDoc1.getPage(1)));
        Assertions.assertEquals(MessageFormatUtil.format(
                KernelExceptionMessageConstant.PAGE_CANNOT_BE_ADDED_TO_DOCUMENT_BECAUSE_IT_BELONGS_TO_ANOTHER_DOCUMENT,
                pdfDoc1, 1, pdfDoc2), exception.getMessage());
    }

    @Test
    public void cannotAddPageToAnotherDocTest() {
        PdfDocument pdfDoc1 = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfDocument pdfDoc2 = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        pdfDoc1.addNewPage(1);
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc2.checkAndAddPage(pdfDoc1.getPage(1)));
        Assertions.assertEquals(MessageFormatUtil.format(
                KernelExceptionMessageConstant.PAGE_CANNOT_BE_ADDED_TO_DOCUMENT_BECAUSE_IT_BELONGS_TO_ANOTHER_DOCUMENT,
                pdfDoc1, 1, pdfDoc2), exception.getMessage());
    }

    @Test
    public void cannotSetEncryptedPayloadInReadingModeTest() throws IOException {
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "setEncryptedPayloadInReadingModeTest.pdf"));
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc.setEncryptedPayload(null));
        Assertions.assertEquals(
                KernelExceptionMessageConstant.CANNOT_SET_ENCRYPTED_PAYLOAD_TO_DOCUMENT_OPENED_IN_READING_MODE,
                exception.getMessage());
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT, 
            ignore = true))
    public void cannotSetEncryptedPayloadToEncryptedDocTest() {
        WriterProperties writerProperties = new WriterProperties();
        writerProperties.setStandardEncryption(new byte[] {}, new byte[] {}, 1, 1);
        PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream(), writerProperties));
        PdfFileSpec fs = PdfFileSpec
                .createExternalFileSpec(pdfDoc, SOURCE_FOLDER + "testPath");
        Exception exception = Assertions.assertThrows(PdfException.class,
                () -> pdfDoc.setEncryptedPayload(fs));
        Assertions.assertEquals(KernelExceptionMessageConstant.CANNOT_SET_ENCRYPTED_PAYLOAD_TO_ENCRYPTED_DOCUMENT,
                exception.getMessage());
    }

    @Test
    public void checkEmptyIsoConformanceTest() {
        try (PdfDocument doc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            IValidationContext validationContext = new PdfDocumentValidationContext(doc, doc.getDocumentFonts());
            AssertUtil.doesNotThrow(() -> doc.checkIsoConformance(validationContext));
        }
    }

    @Test
    public void checkIsoConformanceTest() {
        try (PdfDocument doc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            ValidationContainer container = new ValidationContainer();
            final CustomValidationChecker checker = new CustomValidationChecker();
            container.addChecker(checker);
            doc.getDiContainer().register(ValidationContainer.class, container);
            Assertions.assertFalse(checker.documentValidationPerformed);
            IValidationContext validationContext = new PdfDocumentValidationContext(doc, doc.getDocumentFonts());
            doc.checkIsoConformance(validationContext);
            Assertions.assertTrue(checker.documentValidationPerformed);
        }
    }

    private static class CustomValidationChecker implements IValidationChecker {
        public boolean documentValidationPerformed = false;

        @Override
        public void validate(IValidationContext validationContext) {
            if (validationContext.getType() == ValidationType.PDF_DOCUMENT) {
                documentValidationPerformed = true;
            }
        }

        @Override
        public boolean isPdfObjectReadyToFlush(PdfObject object) {
            return true;
        }
    }
}