PdfImageXObjectTest.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.xobject;

import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.image.ImageType;
import com.itextpdf.io.util.UrlUtil;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;

import java.io.IOException;
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("IntegrationTest")
public class PdfImageXObjectTest extends ExtendedITextTest {
    private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/pdf/xobject/PdfImageXObjectTest/";
    private static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/pdf/xobject/PdfImageXObjectTest/";

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

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

    @Test
    public void addFlushedImageXObjectToCanvas() throws IOException, InterruptedException {
        String filename = DESTINATION_FOLDER + "addFlushedImageXObjectToCanvas.pdf";
        String cmpfile = SOURCE_FOLDER + "cmp_addFlushedImageXObjectToCanvas.pdf";
        String image = SOURCE_FOLDER + "image.png";

        PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(filename));

        PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.create(image));
        // flushing pdf object directly
        imageXObject.getPdfObject().makeIndirect(pdfDoc).flush();

        PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage());

        canvas.addXObjectFittedIntoRectangle(imageXObject, new Rectangle(50, 500, 200, 200));
        pdfDoc.close();

        Assertions.assertNull(new CompareTool().compareByContent(filename, cmpfile, DESTINATION_FOLDER));
    }

    @Test
    public void indexedColorPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "indexed.pdf",
                SOURCE_FOLDER + "cmp_indexed.pdf",
                SOURCE_FOLDER + "indexed.png");
    }

    @Test
    public void indexedColorSimpleTransparencyPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "indexedSimpleTransparency.pdf",
                SOURCE_FOLDER + "cmp_indexedSimpleTransparency.pdf",
                SOURCE_FOLDER + "indexedSimpleTransparency.png");
    }

    @Test
    public void grayPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "grayscale16Bpc.pdf",
                SOURCE_FOLDER + "cmp_grayscale16Bpc.pdf",
                SOURCE_FOLDER + "grayscale16Bpc.png");
    }

    @Test
    public void grayAlphaPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "graya8Bpc.pdf",
                SOURCE_FOLDER + "cmp_graya8Bpc.pdf",
                SOURCE_FOLDER + "graya8Bpc.png");
    }

    @Test
    public void grayAlphaPngWithoutEmbeddedProfileImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "graya8BpcWithoutProfile.pdf",
                SOURCE_FOLDER + "cmp_graya8BpcWithoutProfile.pdf",
                SOURCE_FOLDER + "graya8BpcWithoutProfile.png");
    }

    @Test
    public void graySimpleTransparencyPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "grayscaleSimpleTransparencyImage.pdf",
                SOURCE_FOLDER + "cmp_grayscaleSimpleTransparencyImage.pdf",
                SOURCE_FOLDER + "grayscaleSimpleTransparencyImage.png");
    }

    @Test
    public void rgbPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "rgb16Bpc.pdf",
                SOURCE_FOLDER + "cmp_rgb16Bpc.pdf",
                SOURCE_FOLDER + "rgb16Bpc.png");
    }

    @Test
    public void rgbAlphaPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "rgba16Bpc.pdf",
                SOURCE_FOLDER + "cmp_rgba16Bpc.pdf",
                SOURCE_FOLDER + "rgba16Bpc.png");
    }

    @Test
    public void rgbSimpleTransparencyPngImageXObjectTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "rgbSimpleTransparencyImage.pdf",
                SOURCE_FOLDER + "cmp_rgbSimpleTransparencyImage.pdf",
                SOURCE_FOLDER + "rgbSimpleTransparencyImage.png");
    }

    @Test
    public void sRgbImageTest() throws IOException {
        convertAndCompare(DESTINATION_FOLDER + "sRGBImage.pdf",
                SOURCE_FOLDER + "cmp_sRGBImage.pdf",
                SOURCE_FOLDER + "sRGBImage.png");
    }

    @Test
    public void group3CompressionTiffImageTest() throws IOException {
        String image = SOURCE_FOLDER + "group3CompressionImage.tif";
        convertAndCompare(DESTINATION_FOLDER + "group3CompressionTiffImage.pdf",
                SOURCE_FOLDER + "cmp_group3CompressionTiffImage.pdf",
                new PdfImageXObject(ImageDataFactory.create(UrlUtil.toURL(image))));
    }

    @Test
    public void group3CompTiffImgRecoverErrorAndDirectTest() throws IOException, InterruptedException {
        String filename = DESTINATION_FOLDER + "group3CompTiffImgRecoverErrorAndDirect.pdf";
        String cmpFile = SOURCE_FOLDER + "cmp_group3CompTiffImgRecoverErrorAndDirect.pdf";
        String image = SOURCE_FOLDER + "group3CompressionImage.tif";

        try (PdfWriter writer = CompareTool.createTestPdfWriter(filename);
                PdfDocument pdfDoc = new PdfDocument(writer)) {

            PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.createTiff(UrlUtil.toURL(image),
                    true, 1, true));

            PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage());

            canvas.addXObjectFittedIntoRectangle(imageXObject, new Rectangle(50, 500, 200, 200));
        }

        Assertions.assertNull(new CompareTool().compareByContent(filename, cmpFile, DESTINATION_FOLDER));
    }

    @Test
    public void group3CompTiffImgNoRecoverErrorAndNotDirectTest() throws IOException {
        String image = SOURCE_FOLDER + "group3CompressionImage.tif";

        convertAndCompare(DESTINATION_FOLDER + "group3CompTiffImgNoRecoverErrorAndNotDirect.pdf",
                SOURCE_FOLDER + "cmp_group3CompTiffImgNoRecoverErrorAndNotDirect.pdf",
                new PdfImageXObject(ImageDataFactory.createTiff(UrlUtil.toURL(image),
                        false, 1, false)));
    }

    @Test
    public void redundantDecodeParmsTest() throws IOException, InterruptedException {
        String srcFilename = SOURCE_FOLDER + "redundantDecodeParms.pdf";
        String destFilename = DESTINATION_FOLDER + "redundantDecodeParms.pdf";
        String cmpFilename = SOURCE_FOLDER + "cmp_redundantDecodeParms.pdf";

        try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(srcFilename),
                CompareTool.createTestPdfWriter(destFilename),
                new StampingProperties())) {
        }

        Assertions.assertNull(new CompareTool().compareByContent(destFilename, cmpFilename, DESTINATION_FOLDER));
    }

    @Test
    // Android-Conversion-Ignore-Test (TODO DEVSIX-6445 fix different DeflaterOutputStream behavior)
    public void decodingIndexedCsWithRgbTest() throws IOException {
        try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "img_indexed_rgb.pdf"))) {
            PdfImageXObject imageXObject = pdfDocument.getPage(1).getResources().getImage(new PdfName("Im0"));
            byte[] imageBytes = imageXObject.getImageBytes();
            Assertions.assertNotNull(imageBytes);
            Assertions.assertEquals(552, imageBytes.length);
            Assertions.assertEquals(ImageType.PNG, imageXObject.identifyImageType());
        }
    }

    @Test
    // Android-Conversion-Ignore-Test (TODO DEVSIX-6445 fix different DeflaterOutputStream behavior)
    public void decodingIndexedCsWithRgbStringTableTest() throws IOException {
        try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "img_indexed_rgb_string_table.pdf"))) {
            PdfImageXObject imageXObject = pdfDocument.getPage(1).getResources().getImage(new PdfName("Im0"));
            byte[] imageBytes = imageXObject.getImageBytes();
            Assertions.assertNotNull(imageBytes);
            Assertions.assertEquals(552, imageBytes.length);
            Assertions.assertEquals(ImageType.PNG, imageXObject.identifyImageType());
        }
    }

    @Test
    // Android-Conversion-Ignore-Test (TODO DEVSIX-6445 fix different DeflaterOutputStream behavior)
    public void decodingIndexedCsWithRgbWrongLookupTest() throws IOException {
        try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "img_indexed_rgb_wrong_lookup.pdf"))) {
            PdfImageXObject imageXObject = pdfDocument.getPage(1).getResources().getImage(new PdfName("Im0"));
            byte[] imageBytes = imageXObject.getImageBytes();
            Assertions.assertNotNull(imageBytes);
            // iText doesn't fail if there is no lookup table, it's just ignored and not added to result bytes
            Assertions.assertEquals(531, imageBytes.length);
            Assertions.assertNotEquals(552, imageBytes.length);
            Assertions.assertEquals(ImageType.PNG, imageXObject.identifyImageType());
        }
    }

    @Test
    public void decodingIndexedCsWithRgbNoHivalTest() throws IOException {
        try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(SOURCE_FOLDER + "img_indexed_rgb_null_hival.pdf"))) {
            PdfImageXObject imageXObject = pdfDocument.getPage(1).getResources().getImage(new PdfName("Im0"));
            Assertions.assertThrows(RuntimeException.class, () -> imageXObject.getImageBytes());
        }
    }

    private void convertAndCompare(String outFilename, String cmpFilename, String imageFilename)
            throws IOException {

        System.out.println("Out pdf: " + UrlUtil.getNormalizedFileUriString(outFilename));
        System.out.println("Cmp pdf: " + UrlUtil.getNormalizedFileUriString(cmpFilename)+ "\n");

        PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outFilename));

        PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.create(imageFilename));

        PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage());
        canvas.addXObjectFittedIntoRectangle(imageXObject, new Rectangle(50, 500, 346, imageXObject.getHeight()));
        pdfDoc.close();

        PdfDocument outDoc = new PdfDocument(CompareTool.createOutputReader(outFilename));

        PdfStream outStream = outDoc.getFirstPage().getResources().getResource(PdfName.XObject).getAsStream(new PdfName("Im1"));

        PdfDocument cmpDoc = new PdfDocument(CompareTool.createOutputReader(cmpFilename));
        PdfStream cmpStream = cmpDoc.getFirstPage().getResources().getResource(PdfName.XObject).getAsStream(new PdfName("Im1"));


        Assertions.assertNull(new CompareTool().compareStreamsStructure(outStream, cmpStream));

        cmpDoc.close();
        outDoc.close();
    }

    private void convertAndCompare(String outFilename, String cmpFilename,PdfImageXObject imageXObject )
            throws IOException {

        System.out.println("Out pdf: " + UrlUtil.getNormalizedFileUriString(outFilename));
        System.out.println("Cmp pdf: " + UrlUtil.getNormalizedFileUriString(cmpFilename)+ "\n");

        PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outFilename));


        PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage());
        canvas.addXObjectFittedIntoRectangle(imageXObject, new Rectangle(10, 20, 575 , 802));
        pdfDoc.close();

        PdfDocument outDoc = new PdfDocument(CompareTool.createOutputReader(outFilename));

        PdfStream outStream = outDoc.getFirstPage().getResources().getResource(PdfName.XObject).getAsStream(new PdfName("Im1"));

        PdfDocument cmpDoc = new PdfDocument(CompareTool.createOutputReader(cmpFilename));
        PdfStream cmpStream = cmpDoc.getFirstPage().getResources().getResource(PdfName.XObject).getAsStream(new PdfName("Im1"));


        Assertions.assertNull(new CompareTool().compareStreamsStructure(outStream, cmpStream));

        cmpDoc.close();
        outDoc.close();
    }
}