PdfXObjectTest.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.io.image.ImageData;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
import com.itextpdf.kernel.pdf.xobject.PdfXObject;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;

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

    public static final String[] images = new String[]{SOURCE_FOLDER + "WP_20140410_001.bmp",
            SOURCE_FOLDER + "WP_20140410_001.JPC",
            SOURCE_FOLDER + "WP_20140410_001.jpg",
            SOURCE_FOLDER + "WP_20140410_001.tif"};


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

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

    @Test
    public void createDocumentFromImages1() throws IOException,  InterruptedException {
        final String destinationDocument = DESTINATION_FOLDER + "documentFromImages1.pdf";
        PdfWriter writer = new PdfWriter(destinationDocument);
        PdfDocument document = new PdfDocument(writer);
        PdfImageXObject[] images = new PdfImageXObject[4];
        for (int i = 0; i < 4; i++) {
            images[i] = new PdfImageXObject(ImageDataFactory.create(PdfXObjectTest.images[i]));
            images[i].setLayer(new PdfLayer("layer" + i, document));
            if (i % 2 == 0)
                images[i].flush();
        }
        for (int i = 0; i < 4; i++) {
            PdfPage page = document.addNewPage();
            PdfCanvas canvas = new PdfCanvas(page);
            canvas.addXObjectFittedIntoRectangle(images[i], PageSize.DEFAULT);
            page.flush();
        }
        PdfPage page = document.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.addXObjectFittedIntoRectangle(images[0], new Rectangle(0, 0, 200, 112.35f));
        canvas.addXObjectFittedIntoRectangle(images[1], new Rectangle(300, 0, 200, 112.35f));
        canvas.addXObjectFittedIntoRectangle(images[2], new Rectangle(0, 300, 200, 112.35f));
        canvas.addXObjectFittedIntoRectangle(images[3], new Rectangle(300, 300, 200, 112.35f));
        canvas.release();
        page.flush();
        document.close();

        Assertions.assertTrue(new File(destinationDocument).length() < 20 * 1024 * 1024);
        Assertions.assertNull(new CompareTool().compareByContent(destinationDocument, SOURCE_FOLDER + "cmp_documentFromImages1.pdf",
                DESTINATION_FOLDER, "diff_"));
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.IMAGE_SIZE_CANNOT_BE_MORE_4KB)
    })
    public void createDocumentFromImages2() throws IOException,  InterruptedException {
        final String destinationDocument = DESTINATION_FOLDER + "documentFromImages2.pdf";
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationDocument);
        PdfDocument document = new PdfDocument(writer);

        ImageData image = ImageDataFactory.create(SOURCE_FOLDER + "itext.jpg");
        PdfPage page = document.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.addImageFittedIntoRectangle(image, new Rectangle(50, 500, 100, 14.16f), true);
        canvas.addImageFittedIntoRectangle(image, new Rectangle(200, 500, 100, 14.16f), false).flush();
        canvas.release();
        page.flush();

        document.close();

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

    @Test
    public void createDocumentWithForms() throws IOException,  InterruptedException {
        final String destinationDocument = DESTINATION_FOLDER + "documentWithForms1.pdf";
        PdfWriter writer = CompareTool.createTestPdfWriter(destinationDocument);
        PdfDocument document = new PdfDocument(writer);

        //Create form XObject and flush to document.
        PdfFormXObject form = new PdfFormXObject(new Rectangle(0, 0, 50, 50));
        PdfCanvas canvas = new PdfCanvas(form, document);
        canvas.rectangle(10, 10, 30, 30);
        canvas.fill();
        canvas.release();
        form.flush();

        //Create page1 and add forms to the page.
        PdfPage page1 = document.addNewPage();
        canvas = new PdfCanvas(page1);
        canvas.addXObjectAt(form, 0, 0).addXObjectAt(form, 50, 0).addXObjectAt(form, 0, 50).addXObjectAt(form, 50, 50);
        canvas.release();

        //Create form from the page1 and flush it.
        form = new PdfFormXObject(page1);
        form.flush();

        //Now page1 can be flushed. It's not needed anymore.
        page1.flush();

        //Create page2 and add forms to the page.
        PdfPage page2 = document.addNewPage();
        canvas = new PdfCanvas(page2);
        canvas.addXObjectAt(form, 0, 0);
        canvas.addXObjectAt(form, 0, 200);
        canvas.addXObjectAt(form, 200, 0);
        canvas.addXObjectAt(form, 200, 200);
        canvas.release();
        page2.flush();

        document.close();

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

    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = IoLogMessageConstant.SOURCE_DOCUMENT_HAS_ACROFORM_DICTIONARY))
    public void xObjectIterativeReference() throws IOException {

        // The input file contains circular references chain, see: 8 0 R -> 10 0 R -> 4 0 R -> 8 0 R.
        // Copying of such file even with smart mode is expected to be handled correctly.
        String src = SOURCE_FOLDER + "checkboxes_XObject_iterative_reference.pdf";
        String dest = DESTINATION_FOLDER + "checkboxes_XObject_iterative_reference_out.pdf";

        PdfDocument pdf = new PdfDocument(CompareTool.createTestPdfWriter(dest).setSmartMode(true));
        PdfReader pdfReader = new PdfReader(src);
        PdfDocument sourceDocumentPdf = new PdfDocument(pdfReader);
        sourceDocumentPdf.copyPagesTo(1, sourceDocumentPdf.getNumberOfPages(), pdf);

        //map <object pdf, count>
        HashMap<String, Integer> mapIn = new HashMap<>();
        HashMap<String, Integer> mapOut = new HashMap<>();

        //map <object pdf, list of object id referenceing that podf object>
        HashMap<String, List<Integer>> mapOutId = new HashMap<>();

        PdfObject obj;

        //create helpful data structures from pdf output
        for (int i = 1; i < pdf.getNumberOfPdfObjects(); i++) {
            obj = pdf.getPdfObject(i);
            String objString = obj.toString();
            Integer count = mapOut.get(objString);
            List<Integer> list;

            if (count == null) {
                count = 1;
                list = new ArrayList<Integer>();
                list.add(i);
            } else {
                count++;
                list = mapOutId.get(objString);
            }

            mapOut.put(objString, count);
            mapOutId.put(objString, list);
        }

        //create helpful data structures from pdf input
        for (int i = 1; i < sourceDocumentPdf.getNumberOfPdfObjects(); i++) {
            obj = sourceDocumentPdf.getPdfObject(i);
            String objString = obj.toString();
            Integer count = mapIn.get(objString);

            if (count == null) {
                count = 1;
            } else {
                count++;
            }
            mapIn.put(objString, count);
        }

        pdf.close();

        //the following object is copied and reused. it appears 6 times in the original pdf file. just once in the output file
        String case1 = "<</BBox [0 0 20 20 ] /Filter /FlateDecode /FormType 1 /Length 12 /Matrix [1 0 0 1 0 0 ] /Resources <<>> /Subtype /Form /Type /XObject >>";
        Integer countOut1 = mapOut.get(case1);
        Integer countIn1 = mapIn.get(case1);
        Assertions.assertTrue(countOut1.equals(1) && countIn1.equals(6));

        //the following object appears 1 time in the original pdf file and just once in the output file
        String case2 = "<</BaseFont /ZapfDingbats /Subtype /Type1 /Type /Font >>";
        Integer countOut2 = mapOut.get(case2);
        Integer countIn2 = mapIn.get(case2);
        Assertions.assertTrue(countOut2.equals(countIn2) && countOut2.equals(1));

        //from the original pdf the object "<</BBox [0 0 20 20 ] /Filter /FlateDecode /FormType 1 /Length 70 /Matrix [1 0 0 1 0 0 ] /Resources <</Font <</ZaDb 2 0 R >> >> /Subtype /Form /Type /XObject >>";
        //is going to be found changed in the output pdf referencing the referenced object with another id which is retrieved through the hashmap
        String case3 = "<</BaseFont /ZapfDingbats /Subtype /Type1 /Type /Font >>";
        Integer countIdIn = mapOutId.get(case3).get(0);
        //EXPECTED to be as the original but with different referenced object and marked as modified
        String expected = "<</BBox [0 0 20 20 ] /Filter /FlateDecode /FormType 1 /Length 70 /Matrix [1 0 0 1 0 0 ] /Resources <</Font <</ZaDb " + countIdIn + " 0 R Modified; >> >> /Subtype /Form /Type /XObject >>";
        Assertions.assertTrue(mapOut.get(expected).equals(1));
    }

    @Test
    public void calculateProportionallyFitRectangleWithWidthTest() throws IOException,  InterruptedException {
        final String fileName = "calculateProportionallyFitRectangleWithWidthTest.pdf";
        final String destPdf = DESTINATION_FOLDER + fileName;
        final String cmpPdf = SOURCE_FOLDER + "cmp_" + fileName;
        PdfWriter writer = CompareTool.createTestPdfWriter(destPdf);
        PdfDocument document = new PdfDocument(writer);

        PdfFormXObject formXObject = new PdfFormXObject(new Rectangle(5, 5, 15, 20));
        formXObject.put(PdfName.Matrix, new PdfArray(new float[] {1, 0.57f, 0, 2, 20, 5}));
        new PdfCanvas(formXObject, document).circle(10, 15, 10).fill();

        PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.create(SOURCE_FOLDER + "itext.png"));

        PdfPage page = document.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        Rectangle rect = PdfXObject.calculateProportionallyFitRectangleWithWidth(formXObject, 0, 0, 20);
        canvas.addXObjectFittedIntoRectangle(formXObject, rect);
        rect = PdfXObject.calculateProportionallyFitRectangleWithWidth(imageXObject, 20, 0, 20);
        canvas.addXObjectFittedIntoRectangle(imageXObject, rect);

        canvas.release();
        page.flush();

        document.close();

        Assertions.assertNull(new CompareTool().compareByContent(destPdf, cmpPdf, DESTINATION_FOLDER, "diff_"));
    }

    @Test
    @Tag("UnitTest")
    public void calculateProportionallyFitRectangleWithWidthForCustomXObjectTest() {
        PdfXObject pdfXObject = new CustomPdfXObject(new PdfStream());

        Exception e = Assertions.assertThrows(IllegalArgumentException.class,
                () -> PdfXObject.calculateProportionallyFitRectangleWithWidth(pdfXObject, 0, 0, 20)
        );
        Assertions.assertEquals("PdfFormXObject or PdfImageXObject expected.", e.getMessage());
    }

    @Test
    public void calculateProportionallyFitRectangleWithHeightTest() throws IOException,  InterruptedException {
        final String fileName = "calculateProportionallyFitRectangleWithHeightTest.pdf";
        final String destPdf = DESTINATION_FOLDER + fileName;
        final String cmpPdf = SOURCE_FOLDER + "cmp_" + fileName;
        PdfWriter writer = CompareTool.createTestPdfWriter(destPdf);
        PdfDocument document = new PdfDocument(writer);

        PdfFormXObject formXObject = new PdfFormXObject(new Rectangle(5, 5, 15, 20));
        formXObject.put(PdfName.Matrix, new PdfArray(new float[] {1, 0.57f, 0, 2, 20, 5}));
        new PdfCanvas(formXObject, document).circle(10, 15, 10).fill();

        PdfImageXObject imageXObject = new PdfImageXObject(ImageDataFactory.create(SOURCE_FOLDER + "itext.png"));

        PdfPage page = document.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page);
        Rectangle rect = PdfXObject.calculateProportionallyFitRectangleWithHeight(formXObject, 0, 0, 20);
        canvas.addXObjectFittedIntoRectangle(formXObject, rect);
        rect = PdfXObject.calculateProportionallyFitRectangleWithHeight(imageXObject, 20, 0, 20);
        canvas.addXObjectFittedIntoRectangle(imageXObject, rect);

        canvas.release();
        page.flush();

        document.close();

        Assertions.assertNull(new CompareTool().compareByContent(destPdf, cmpPdf, DESTINATION_FOLDER, "diff_"));
    }

    @Test
    @Tag("UnitTest")
    public void calculateProportionallyFitRectangleWithHeightForCustomXObjectTest() {
        PdfXObject pdfXObject = new CustomPdfXObject(new PdfStream());

        Exception e = Assertions.assertThrows(IllegalArgumentException.class,
                () -> PdfXObject.calculateProportionallyFitRectangleWithHeight(pdfXObject, 0, 0, 20)
        );
        Assertions.assertEquals("PdfFormXObject or PdfImageXObject expected.", e.getMessage());
    }

    private static class CustomPdfXObject extends PdfXObject {
        protected CustomPdfXObject(PdfStream pdfObject) {
            super(pdfObject);
        }
    }
}