PdfStreamTest.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.pdf.xobject.PdfImageXObject;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import com.itextpdf.test.LogLevelConstants;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;

import java.util.Collections;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
import static org.junit.jupiter.api.Assertions.fail;

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

    public static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/pdf/PdfStreamTest/";
    public static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/pdf/PdfStreamTest/";

    @BeforeAll
    public static void before() {
        createOrClearDestinationFolder(destinationFolder);
    }

    @AfterAll
    public static void afterClass() {
        CompareTool.cleanup(destinationFolder);
    }
    
    @Test
    public void streamAppendDataOnJustCopiedWithCompression() throws IOException, InterruptedException {
        String srcFile = sourceFolder + "pageWithContent.pdf";
        String cmpFile = sourceFolder + "cmp_streamAppendDataOnJustCopiedWithCompression.pdf";
        String destFile = destinationFolder + "streamAppendDataOnJustCopiedWithCompression.pdf";

        PdfDocument srcDocument = new PdfDocument(new PdfReader(srcFile));
        PdfDocument document = new PdfDocument(CompareTool.createTestPdfWriter(destFile));
        srcDocument.copyPagesTo(1, 1, document);
        srcDocument.close();

        String newContentString = "BT\n" +
                "/F1 36 Tf\n" +
                "50 700 Td\n" +
                "(new content here!) Tj\n" +
                "ET";
        byte[] newContent = newContentString.getBytes(StandardCharsets.UTF_8);
        document.getPage(1).getLastContentStream().setData(newContent, true);

        document.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder, "diff_"));
    }

    @Test
    // Android-Conversion-Ignore-Test (TODO DEVSIX-6445 fix different DeflaterOutputStream behavior)
    public void runLengthEncodingTest01() throws IOException {
        String srcFile = sourceFolder + "runLengthEncodedImages.pdf";

        PdfDocument document = new PdfDocument(new PdfReader(srcFile));

        PdfImageXObject im1 = document.getPage(1).getResources().getImage(new PdfName("Im1"));
        PdfImageXObject im2 = document.getPage(1).getResources().getImage(new PdfName("Im2"));

        byte[] imgBytes1 = im1.getImageBytes();
        byte[] imgBytes2 = im2.getImageBytes();

        document.close();

        byte[] cmpImgBytes1 = readFile(sourceFolder + "cmp_img1.jpg");
        byte[] cmpImgBytes2 = readFile(sourceFolder + "cmp_img2.jpg");

        Assertions.assertArrayEquals(imgBytes1, cmpImgBytes1);
        Assertions.assertArrayEquals(imgBytes2, cmpImgBytes2);
    }

    @Test
    public void indirectRefInFilterAndNoTaggedPdfTest() throws IOException {
        String inFile = sourceFolder + "indirectRefInFilterAndNoTaggedPdf.pdf";
        String outFile = destinationFolder + "destIndirectRefInFilterAndNoTaggedPdf.pdf";

        PdfDocument srcDoc = new PdfDocument(new PdfReader(inFile));
        PdfDocument outDoc = new PdfDocument(new PdfReader(inFile), CompareTool.createTestPdfWriter(outFile));
        outDoc.close();

        PdfDocument doc = new PdfDocument(CompareTool.createOutputReader(outFile));

        PdfStream outStreamIm1 = doc.getFirstPage().getResources().getResource(PdfName.XObject)
                .getAsStream(new PdfName("Im1"));
        PdfStream outStreamIm2 = doc.getFirstPage().getResources().getResource(PdfName.XObject)
                .getAsStream(new PdfName("Im2"));

        PdfStream cmpStreamIm1 = srcDoc.getFirstPage().getResources().getResource(PdfName.XObject)
                .getAsStream(new PdfName("Im1"));
        PdfStream cmpStreamIm2 = srcDoc.getFirstPage().getResources().getResource(PdfName.XObject)
                .getAsStream(new PdfName("Im2"));

        Assertions.assertNull(new CompareTool().compareStreamsStructure(outStreamIm1, cmpStreamIm1));
        Assertions.assertNull(new CompareTool().compareStreamsStructure(outStreamIm2, cmpStreamIm2));

        srcDoc.close();
        outDoc.close();
    }

    @Test
    public void cryptFilterFlushedBeforeReadStreamTest() throws IOException {
        String file = sourceFolder + "cryptFilterTest.pdf";
        String destFile = destinationFolder + "cryptFilterReadStreamTest.pdf";

        PdfReader reader = new com.itextpdf.kernel.pdf.PdfReader(file,
                new ReaderProperties().setPassword("World".getBytes(StandardCharsets.ISO_8859_1)));
        int encryptionType = EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
        int permissions = EncryptionConstants.ALLOW_SCREENREADERS;
        WriterProperties writerProperties = new WriterProperties().setStandardEncryption(
                "World".getBytes(StandardCharsets.ISO_8859_1),
                "Hello".getBytes(StandardCharsets.ISO_8859_1), permissions, encryptionType);
        PdfWriter writer = CompareTool.createTestPdfWriter(destFile, writerProperties.addXmpMetadata());

        PdfDocument doc = new PdfDocument(reader, writer);
        ((PdfStream)doc.getPdfObject(5)).getBytes();
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfStream)doc.getPdfObject(5)).get(PdfName.Filter).flush();
        Exception exception = Assertions.assertThrows(PdfException.class, () -> doc.close());
        Assertions.assertEquals(
                MessageFormatUtil.format(KernelExceptionMessageConstant.FLUSHED_STREAM_FILTER_EXCEPTION, "5", "0"),
                exception.getMessage());
    }

    @Test
    public void cryptFilterFlushedBeforeStreamTest() throws IOException {
        String file = sourceFolder + "cryptFilterTest.pdf";
        String destFile = destinationFolder + "cryptFilterStreamNotReadTest.pdf";

        PdfReader reader = new com.itextpdf.kernel.pdf.PdfReader(file,
                new ReaderProperties().setPassword("World".getBytes(StandardCharsets.ISO_8859_1)));
        int encryptionType = EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
        int permissions = EncryptionConstants.ALLOW_SCREENREADERS;
        WriterProperties writerProperties = new WriterProperties().setStandardEncryption(
                "World".getBytes(StandardCharsets.ISO_8859_1),
                "Hello".getBytes(StandardCharsets.ISO_8859_1), permissions, encryptionType);
        PdfWriter writer = CompareTool.createTestPdfWriter(destFile, writerProperties.addXmpMetadata());

        PdfDocument doc = new PdfDocument(reader, writer);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfStream)doc.getPdfObject(5)).get(PdfName.Filter).flush();
        Exception exception = Assertions.assertThrows(PdfException.class, () -> doc.close());
        Assertions.assertEquals(
                MessageFormatUtil.format(KernelExceptionMessageConstant.FLUSHED_STREAM_FILTER_EXCEPTION, "5", "0"),
                exception.getMessage());
    }

    @Test
    public void cryptFilterFlushedAfterStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "cryptFilterTest.pdf";
        String cmpFile = sourceFolder + "cmp_cryptFilterTest.pdf";
        String destFile = destinationFolder + "cryptFilterTest.pdf";
        byte[] user = "Hello".getBytes(StandardCharsets.ISO_8859_1);
        byte[] owner = "World".getBytes(StandardCharsets.ISO_8859_1);

        PdfReader reader = new com.itextpdf.kernel.pdf.PdfReader(file,
                new ReaderProperties().setPassword(owner));
        int encryptionType = EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
        int permissions = EncryptionConstants.ALLOW_SCREENREADERS;
        WriterProperties writerProperties = new WriterProperties().setStandardEncryption(user, owner, permissions,
                encryptionType);
        PdfWriter writer = CompareTool.createTestPdfWriter(destFile, writerProperties.addXmpMetadata());
        writer.setCompressionLevel(-1);

        PdfDocument doc = new PdfDocument(reader, writer);
        PdfObject cryptFilter = ((PdfStream)doc.getPdfObject(5)).get(PdfName.Filter);
        doc.getPdfObject(5).flush();
        //Simulating that this flush happened automatically before normal stream flushing in close method
        cryptFilter.flush();
        doc.close();
        CompareTool compareTool = new CompareTool().enableEncryptionCompare();
        String compareResult = compareTool.compareByContent(destFile, cmpFile, destinationFolder, "diff_", user, user);
        if (compareResult != null) {
            fail(compareResult);
        }
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void indirectFilterInCatalogTest() throws IOException, InterruptedException {
        String file = sourceFolder + "indFilterInCatalog.pdf";
        String cmpFile = sourceFolder + "cmp_indFilterInCatalog.pdf";
        String destFile = destinationFolder + "indFilterInCatalog.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void userDefinedCompressionWithIndirectFilterInCatalogTest() throws IOException, InterruptedException {
        String file = sourceFolder + "indFilterInCatalog.pdf";
        String cmpFile = sourceFolder + "cmp_indFilterInCatalog.pdf";
        String destFile = destinationFolder + "indFilterInCatalog.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(5);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void indirectFilterFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "indFilterInCatalog.pdf";
        String cmpFile = sourceFolder + "cmp_indFilterInCatalog.pdf";
        String destFile = destinationFolder + "indFilterInCatalog.pdf";

        PdfDocument pdfDoc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));

        // Simulate the case in which filter is somehow already flushed before stream.
        // Either directly by user or because of any other reason.
        PdfObject filterObject = pdfDoc.getPdfObject(6);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        filterObject.flush();
        pdfDoc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void indirectFilterMarkedToBeFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "indFilterInCatalog.pdf";
        String cmpFile = sourceFolder + "cmp_indFilterInCatalog.pdf";
        String destFile = destinationFolder + "indFilterInCatalog.pdf";

        PdfWriter writer = CompareTool.createTestPdfWriter(destFile);
        PdfDocument pdfDoc = new PdfDocument(new PdfReader(file), writer);

        // Simulate the case when indirect filter object is marked to be flushed before the stream itself.
        PdfObject filterObject = pdfDoc.getPdfObject(6);
        filterObject.getIndirectReference().setState(PdfObject.MUST_BE_FLUSHED);

        // The image stream will be marked as MUST_BE_FLUSHED after page is flushed.
        pdfDoc.getFirstPage().getPdfObject().getIndirectReference().setState(PdfObject.MUST_BE_FLUSHED);

        // There was a NPE because FlateFilter was already flushed.
        writer.flushWaitingObjects(Collections.<PdfIndirectReference>emptySet());
        // There also was a NPE because FlateFilter was already flushed.
        pdfDoc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void decodeParamsFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_decodeParamsTest.pdf";
        String destFile = destinationFolder + "decodeParamsTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        stream.get(PdfName.DecodeParms).makeIndirect(stream.getIndirectReference().getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void decodeParamsPredictorFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_decodeParamsPredictorTest.pdf";
        String destFile = destinationFolder + "decodeParamsPredictorTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfDictionary)stream.get(PdfName.DecodeParms)).get(PdfName.Predictor).makeIndirect(stream.getIndirectReference()
                .getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void decodeParamsColumnsFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_decodeParamsColumnsTest.pdf";
        String destFile = destinationFolder + "decodeParamsColumnsTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfDictionary)stream.get(PdfName.DecodeParms)).get(PdfName.Columns).makeIndirect(stream.getIndirectReference()
                .getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void decodeParamsColorsFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_decodeParamsColorsTest.pdf";
        String destFile = destinationFolder + "decodeParamsColorsTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfDictionary)stream.get(PdfName.DecodeParms)).get(PdfName.Colors).makeIndirect(stream.getIndirectReference()
                .getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void decodeParamsBitsPerComponentFlushedBeforeStreamTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_decodeParamsBitsPerComponentTest.pdf";
        String destFile = destinationFolder + "decodeParamsBitsPerComponentTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        //Simulating that this flush happened automatically before normal stream flushing in close method
        ((PdfDictionary)stream.get(PdfName.DecodeParms)).get(PdfName.BitsPerComponent).makeIndirect(stream.getIndirectReference()
                .getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void flateFilterFlushedWhileDecodeTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_flateFilterFlushedWhileDecodeTest.pdf";
        String destFile = destinationFolder + "flateFilterFlushedWhileDecodeTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        stream.remove(PdfName.Filter);
        stream.put(PdfName.Filter, new PdfName(PdfName.FlateDecode.value));
        //Simulating that this flush happened automatically before normal stream flushing in close method
        stream.get(PdfName.Filter).makeIndirect(stream.getIndirectReference().getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }

    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.FILTER_WAS_ALREADY_FLUSHED, count = 2, logLevel =
                    LogLevelConstants.INFO)
    })
    @Test
    public void arrayFlateFilterFlushedWhileDecodeTest() throws IOException, InterruptedException {
        String file = sourceFolder + "decodeParamsTest.pdf";
        String cmpFile = sourceFolder + "cmp_arrayFlateFilterFlushedWhileDecodeTest.pdf";
        String destFile = destinationFolder + "arrayFlateFilterFlushedWhileDecodeTest.pdf";

        PdfDocument doc = new PdfDocument(new PdfReader(file), CompareTool.createTestPdfWriter(destFile));
        PdfStream stream = (PdfStream) doc.getPdfObject(7);
        stream.setCompressionLevel(CompressionConstants.BEST_COMPRESSION);
        stream.remove(PdfName.Filter);
        stream.put(PdfName.Filter, new PdfArray(new PdfName(PdfName.FlateDecode.value)));
        //Simulating that this flush happened automatically before normal stream flushing in close method
        stream.get(PdfName.Filter).makeIndirect(stream.getIndirectReference().getDocument()).flush();
        doc.close();
        Assertions.assertNull(new CompareTool().compareByContent(destFile, cmpFile, destinationFolder));
    }
}