TrailerTest.java

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

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

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

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf;

import com.itextpdf.commons.actions.data.ProductData;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.kernel.actions.data.ITextCoreProductData;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.test.ExtendedITextTest;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map.Entry;

import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

@Tag("IntegrationTest")
public class TrailerTest extends ExtendedITextTest {

    public static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/pdf/TrailerTest/";
    private static final byte[] USERPASS = "user".getBytes();
    private static final byte[] OWNERPASS = "owner".getBytes();

    @BeforeAll
    public static void beforeClass() {
        createDestinationFolder(destinationFolder);
    }

    @Test
    public void trailerFingerprintTest() throws IOException {
        ProductData productData = new ProductData("pdfProduct", "pdfProduct", "1.0.0", 1900, 2000);
        PdfDocument pdf = new PdfDocument(new PdfWriter(destinationFolder + "output.pdf"));
        pdf.registerProduct(productData);
        PdfPage page = pdf.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.beginText()
                .setFontAndSize(PdfFontFactory.createFont(), 12f)
                .showText("Hello World")
                .endText();

        pdf.close();

        Assertions.assertTrue(doesTrailerContainFingerprint(new File(destinationFolder + "output.pdf"), MessageFormatUtil
                .format("%iText-{0}-{1}\n", productData.getProductName(), productData.getVersion())));
    }

    @Test
    /**
     * This tests if iText will keep the all entries in the trailer dictionary
     * while stamping a document that has custom or non-mandatory entries in the trailer.
     */
    public void existingTrailerValuesTest() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PdfName expectedKey = new PdfName("Custom");
        PdfName expectedValue = new PdfName("Value");
        try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(baos));) {
            pdfDocument.getTrailer().put(expectedKey, expectedValue);
        }
        try (PdfDocument stampingDocument = new PdfDocument(
                new PdfReader(new ByteArrayInputStream(baos.toByteArray())),
                new PdfWriter(new ByteArrayOutputStream()));
        ) {
            PdfDictionary trailer = stampingDocument.getTrailer();
            boolean keyPresent = trailer.containsKey(expectedKey);
            PdfName actualValue = trailer.getAsName(expectedKey);
            stampingDocument.close();
            Assertions.assertTrue(keyPresent);
            Assertions.assertEquals(expectedValue, actualValue);
        }
    }

    @Test
    /**
     * This tests if iText will keep the all entries in the trailer dictionary
     * while stamping a document that has custom or non-mandatory entries in the trailer.
     */
    public void existingTrailerValuesTestWithEncryption() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WriterProperties writerProperties = new WriterProperties();
        writerProperties.setStandardEncryption(USERPASS, OWNERPASS, EncryptionConstants.ALLOW_PRINTING,
                EncryptionConstants.ENCRYPTION_AES_128);
        PdfName expectedKey = new PdfName("Custom");
        PdfName expectedValue = new PdfName("Value");

        try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(baos, writerProperties))) {
            pdfDocument.getTrailer().put(expectedKey, expectedValue);
        }
        ReaderProperties readerProperties = new ReaderProperties().setPassword(OWNERPASS);
        try (PdfDocument stampingDocument = new PdfDocument(
                new PdfReader(new ByteArrayInputStream(baos.toByteArray()), readerProperties),
                new PdfWriter(new ByteArrayOutputStream())
        )) {
            PdfDictionary trailer = stampingDocument.getTrailer();
            boolean keyPresent = trailer.containsKey(expectedKey);
            PdfName actualValue = trailer.getAsName(expectedKey);

            stampingDocument.close();

            Assertions.assertTrue(keyPresent);
            Assertions.assertEquals(expectedValue, actualValue);
        }
    }

    @Test
    public void existingTrailerValuesWithStandardizedNameTest() throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        HashMap<PdfName, PdfName> standardizedNames = new HashMap<>();
        //some standardized names to put in the trailer, but they may not be removed
        standardizedNames.put(PdfName.Color, new PdfName("brown"));
        standardizedNames.put(PdfName.BaseFont, new PdfName("CustomFont"));
        standardizedNames.put(PdfName.Pdf_Version_1_6, new PdfName("1.6"));

        try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(baos));) {
            for (Entry<PdfName, PdfName> entry : standardizedNames.entrySet()) {
                PdfName pdfName = entry.getKey();
                PdfName s = entry.getValue();
                pdfDocument.getTrailer().put(pdfName, s);
            }
        }
        try (PdfDocument stampingDocument = new PdfDocument(
                new PdfReader(new ByteArrayInputStream(baos.toByteArray())),
                new PdfWriter(new ByteArrayOutputStream()));
        ) {
            PdfDictionary trailer = stampingDocument.getTrailer();
            for (Entry<PdfName, PdfName> entry : standardizedNames.entrySet()) {
                PdfName pdfName = entry.getKey();
                PdfName pdfName2 = entry.getValue();
                boolean keyPresent = trailer.containsKey(pdfName);
                PdfName actualValue = trailer.getAsName(pdfName);
                Assertions.assertTrue(keyPresent);
                Assertions.assertEquals(pdfName2, actualValue);
            }
            stampingDocument.close();
        }
    }

    @Test
    public void enableFingerprintInAGPLModeTest() throws IOException {
        ProductData productData = new ProductData("pdfProduct", "pdfProduct", "1.0.0", 1900, 2000);
        PdfDocument pdf = new PdfDocument(new PdfWriter(
                destinationFolder + "enableFingerprintInAGPLMode.pdf"));
        pdf.registerProduct(productData);
        PdfPage page = pdf.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.beginText()
                .setFontAndSize(PdfFontFactory.createFont(), 12f)
                .showText("Hello World")
                .endText();
        pdf.close();

        Assertions.assertTrue(doesTrailerContainFingerprint(new File(
                destinationFolder + "enableFingerprintInAGPLMode.pdf"), MessageFormatUtil.format(
                        "%iText-{0}-{1}\n", productData.getProductName(), productData.getVersion())));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate =
            KernelLogMessageConstant.FINGERPRINT_DISABLED_BUT_NO_REQUIRED_LICENCE))
    public void tryDisablingFingerprintInAGPLModeTest() throws IOException {
        ProductData productData = new ProductData("pdfProduct", "pdfProduct", "1.0.0", 1900, 2000);
        PdfDocument pdf = new PdfDocument(new PdfWriter(
                destinationFolder + "tryDisablingFingerprintInAGPLMode.pdf"));
        pdf.registerProduct(productData);
        PdfPage page = pdf.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.beginText()
                .setFontAndSize(PdfFontFactory.createFont(), 12f)
                .showText("Hello World")
                .endText();
        pdf.getFingerPrint().disableFingerPrint();
        pdf.close();


        Assertions.assertTrue(doesTrailerContainFingerprint(new File(
                destinationFolder + "tryDisablingFingerprintInAGPLMode.pdf"), MessageFormatUtil.format(
                "%iText-{0}-{1}\n", productData.getProductName(), productData.getVersion())));
    }

    private boolean doesTrailerContainFingerprint(File file, String fingerPrint) throws IOException {
        try (RandomAccessFile raf = FileUtil.getRandomAccessFile(file)) {

            // put the pointer at the end of the file
            raf.seek(raf.length());

            // look for coreProductData
            String coreProductData = "%iText-Core-" + ITextCoreProductData.getInstance().getVersion();
            String templine = "";

            while (!templine.contains(coreProductData)) {
                if (raf.getFilePointer() <= 2) {
                    return false;
                }

                templine = (char) raf.read() + templine;
                raf.seek(raf.getFilePointer() - 2);
            }

            // look for fingerprint
            char read = ' ';
            templine = "";

            while (read != '%') {
                if (raf.getFilePointer() <= 2) {
                    return false;
                }

                read = (char) raf.read();
                templine = read + templine;
                raf.seek(raf.getFilePointer() - 2);
            }

            return templine.contains(fingerPrint);
        }
    }
}