CompareToolTest.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.utils;

import com.itextpdf.commons.utils.SystemUtil;
import com.itextpdf.io.exceptions.IoExceptionMessageConstant;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.util.GhostscriptHelper;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfDocumentInfo;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.navigation.PdfExplicitDestination;
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 javax.xml.parsers.ParserConfigurationException;
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;
import org.xml.sax.SAXException;

@Tag("IntegrationTest")
// Android-Conversion-Skip-File (during Android conversion the class will be replaced by DeferredCompareTool)
public class CompareToolTest extends ExtendedITextTest {

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

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

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

    @Test
    public void compareToolErrorReportTest01()
            throws InterruptedException, IOException, ParserConfigurationException, SAXException {
        CompareTool compareTool = new CompareTool();
        compareTool.setCompareByContentErrorsLimit(10);
        compareTool.setGenerateCompareByContentXmlReport(true);
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";
        String result = compareTool.compareByContent(outPdf, cmpPdf, destinationFolder);
        System.out.println("\nRESULT:\n" + result);
        Assertions.assertNotNull("CompareTool must return differences found between the files", result);
        Assertions.assertTrue(result.contains("differs on page [1, 2]."));
        // Comparing the report to the reference one.
        Assertions.assertTrue(
                compareTool.compareXmls(destinationFolder + "simple_pdf.report.xml", sourceFolder + "cmp_report01.xml"),
                "CompareTool report differs from the reference one");
    }

    @Test
    public void compareToolErrorReportTest02()
            throws IOException, InterruptedException, ParserConfigurationException, SAXException {
        CompareTool compareTool = new CompareTool();
        compareTool.setCompareByContentErrorsLimit(10);
        compareTool.setGenerateCompareByContentXmlReport(true);
        String outPdf = sourceFolder + "tagged_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_tagged_pdf.pdf";
        String result = compareTool.compareByContent(outPdf, cmpPdf, destinationFolder);
        System.out.println("\nRESULT:\n" + result);
        Assertions.assertNotNull("CompareTool must return differences found between the files", result);
        Assertions.assertTrue(result.contains("Compare by content fails. No visual differences"));
        // Comparing the report to the reference one.
        Assertions.assertTrue(
                compareTool.compareXmls(destinationFolder + "tagged_pdf.report.xml", sourceFolder + "cmp_report02.xml"),
                "CompareTool report differs from the reference one");
    }

    @Test
    public void compareToolErrorReportTest03()
            throws InterruptedException, IOException, ParserConfigurationException, SAXException {
        CompareTool compareTool = new CompareTool();
        compareTool.setCompareByContentErrorsLimit(10);
        compareTool.setGenerateCompareByContentXmlReport(true);
        String outPdf = sourceFolder + "screenAnnotation.pdf";
        String cmpPdf = sourceFolder + "cmp_screenAnnotation.pdf";
        String result = compareTool.compareByContent(outPdf, cmpPdf, destinationFolder);
        System.out.println("\nRESULT:\n" + result);
        Assertions.assertNotNull("CompareTool must return differences found between the files", result);
        Assertions.assertTrue(result.contains("Compare by content fails. No visual differences"));
        // Comparing the report to the reference one.
        Assertions.assertTrue(compareTool.compareXmls(destinationFolder + "screenAnnotation.report.xml",
                sourceFolder + "cmp_report03.xml"), "CompareTool report differs from the reference one");
    }


    @Test
    // Test space in name
    public void compareToolErrorReportTest04()
            throws InterruptedException, IOException, ParserConfigurationException, SAXException {
        CompareTool compareTool = new CompareTool();
        compareTool.setCompareByContentErrorsLimit(10);
        compareTool.setGenerateCompareByContentXmlReport(true);
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf_with_space .pdf";
        String result = compareTool.compareByContent(outPdf, cmpPdf, destinationFolder);
        System.out.println("\nRESULT:\n" + result);
        Assertions.assertNotNull("CompareTool must return differences found between the files", result);
        Assertions.assertTrue(result.contains("differs on page [1, 2]."));
        // Comparing the report to the reference one.
        Assertions.assertTrue(
                compareTool.compareXmls(destinationFolder + "simple_pdf.report.xml", sourceFolder + "cmp_report01.xml"),
                "CompareTool report differs from the reference one");

    }

    @Test
    public void differentProducerTest() throws IOException {
        String expectedMessage = "Document info fail. Expected: \"iText\u00ae <version> \u00a9<copyright years> Apryse Group NV (iText Software; licensed version)\", actual: \"iText\u00ae <version> \u00a9<copyright years> Apryse Group NV (AGPL-version)\"";
        String licensed = sourceFolder + "producerLicensed.pdf";
        String agpl = sourceFolder + "producerAGPL.pdf";
        Assertions.assertEquals(expectedMessage, new CompareTool().compareDocumentInfo(agpl, licensed));
    }

    @Test
    public void versionReplaceTest() {
        String initial = "iText�� 1.10.10-SNAPSHOT (licensed to iText) ��2000-2018 Apryse Group NV";
        String replacedExpected = "iText�� <version> (licensed to iText) ��<copyright years> Apryse Group NV";
        Assertions.assertEquals(replacedExpected, new CompareTool().convertProducerLine(initial));
    }

    @Test
    public void gsEnvironmentVariableIsNotSpecifiedExceptionTest() throws IOException, InterruptedException {
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";
        new CompareTool(null, null).compareVisually(outPdf, cmpPdf, destinationFolder, "diff_");
        Assertions.assertTrue(new File(destinationFolder + "diff_1.png").exists());
    }

    @Test
    public void compareXmpThrows(){
        CompareTool compareTool = new CompareTool();
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";
        Assertions.assertEquals("XMP parsing failure!", compareTool.compareXmp(outPdf, cmpPdf));
    }

    @Test
    public void gsEnvironmentVariableSpecifiedIncorrectlyTest() throws IOException, InterruptedException {
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";

        Exception e = Assertions.assertThrows(CompareTool.CompareToolExecutionException.class,
                () -> new CompareTool("unspecified", null).compareVisually(outPdf, cmpPdf, destinationFolder, "diff_")
        );
        Assertions.assertEquals(IoExceptionMessageConstant.GS_ENVIRONMENT_VARIABLE_IS_NOT_SPECIFIED, e.getMessage());
    }

    @Test
    public void compareCommandIsNotSpecifiedTest() throws IOException, InterruptedException {
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";
        String gsExec = SystemUtil.getPropertyOrEnvironmentVariable(GhostscriptHelper.GHOSTSCRIPT_ENVIRONMENT_VARIABLE);
        if (gsExec == null) {
            gsExec = SystemUtil.getPropertyOrEnvironmentVariable("gsExec");
        }
        String result = new CompareTool(gsExec, null)
                .compareVisually(outPdf, cmpPdf, destinationFolder, "diff_");
        Assertions.assertFalse(result.contains(IoExceptionMessageConstant.COMPARE_COMMAND_IS_NOT_SPECIFIED));
        Assertions.assertTrue(new File(destinationFolder + "diff_1.png").exists());
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoExceptionMessageConstant.COMPARE_COMMAND_SPECIFIED_INCORRECTLY)})
    public void compareCommandSpecifiedIncorrectlyTest() throws IOException, InterruptedException {
        String outPdf = sourceFolder + "simple_pdf.pdf";
        String cmpPdf = sourceFolder + "cmp_simple_pdf.pdf";
        String gsExec = SystemUtil.getPropertyOrEnvironmentVariable(GhostscriptHelper.GHOSTSCRIPT_ENVIRONMENT_VARIABLE);
        if (gsExec == null) {
            gsExec = SystemUtil.getPropertyOrEnvironmentVariable("gsExec");
        }
        String result = new CompareTool(gsExec, "unspecified")
                .compareVisually(outPdf, cmpPdf, destinationFolder, "diff_");
        Assertions.assertTrue(result.contains(IoExceptionMessageConstant.COMPARE_COMMAND_SPECIFIED_INCORRECTLY));
    }

    @Test
    public void compareVisuallyDiffTestTest() throws IOException, InterruptedException {
        String outPdf = sourceFolder + "compareVisuallyDiffTestTest1.pdf";
        String cmpPdf = sourceFolder + "compareVisuallyDiffTestTest2.pdf";
        String result = new CompareTool().compareVisually(outPdf, cmpPdf, destinationFolder, "diff_");
        System.out.println("\nRESULT:\n" + result);
        Assertions.assertTrue(result.contains("differs on page [1, 2]."));
        Assertions.assertTrue(new File(destinationFolder + "diff_1.png").exists());
        Assertions.assertTrue(new File(destinationFolder + "diff_2.png").exists());
    }

    @Test
    public void compareDiffFilesWithSameLinkAnnotationTest() throws IOException {
        String firstPdf = destinationFolder + "firstPdf.pdf";
        String secondPdf = destinationFolder + "secondPdf.pdf";
        PdfDocument firstDocument = new PdfDocument(CompareTool.createTestPdfWriter(firstPdf));

        PdfPage page1FirstDocument = firstDocument.addNewPage();
        PdfCanvas canvas = new PdfCanvas(page1FirstDocument);
        canvas.beginText();
        canvas.setFontAndSize(PdfFontFactory.createFont(StandardFonts.COURIER_BOLD), 14);
        canvas.moveText(100, 600);
        canvas.showText("Page 1");
        canvas.moveText(0, -30);
        canvas.showText("Link to page 1. Click here!");
        canvas.endText();
        canvas.release();
        page1FirstDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 260, 25)).setDestination(
                PdfExplicitDestination.createFit(page1FirstDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1FirstDocument.flush();
        firstDocument.close();

        PdfDocument secondDocument = new PdfDocument(CompareTool.createTestPdfWriter(secondPdf));
        PdfPage page1secondDocument = secondDocument.addNewPage();
        canvas = new PdfCanvas(page1secondDocument);
        canvas.beginText();
        canvas.setFontAndSize(PdfFontFactory.createFont(StandardFonts.COURIER_BOLD), 14);
        canvas.moveText(100, 600);
        canvas.showText("Page 1 wit different Text");
        canvas.moveText(0, -30);
        canvas.showText("Link to page 1. Click here!");
        canvas.endText();
        canvas.release();
        page1secondDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 260, 25)).setDestination(
                PdfExplicitDestination.createFit(page1secondDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1secondDocument.flush();
        secondDocument.close();

        Assertions.assertNull(new CompareTool().compareLinkAnnotations(firstPdf, secondPdf));
    }

    @Test
    public void compareFilesWithDiffLinkAnnotationTest() throws IOException {
        String firstPdf = destinationFolder + "outPdf.pdf";
        String secondPdf = destinationFolder + "secondPdf.pdf";
        PdfDocument firstDocument = new PdfDocument(CompareTool.createTestPdfWriter(firstPdf));
        PdfDocument secondDocument = new PdfDocument(CompareTool.createTestPdfWriter(secondPdf));

        PdfPage page1FirstDocument = firstDocument.addNewPage();
        page1FirstDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 400, 50)).setDestination(
                PdfExplicitDestination.createFit(page1FirstDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1FirstDocument.flush();
        firstDocument.close();

        PdfPage page1SecondDocument = secondDocument.addNewPage();
        page1SecondDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 260, 25)).setDestination(
                PdfExplicitDestination.createFit(page1SecondDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1SecondDocument.flush();
        secondDocument.close();

        Assertions.assertNotNull(new CompareTool().compareLinkAnnotations(firstPdf, secondPdf));
    }

    @Test
    public void convertDocInfoToStringsTest() throws IOException {
        String inPdf = sourceFolder + "test.pdf";

        class TestCompareTool extends CompareTool {
            @Override
            protected String[] convertDocInfoToStrings(PdfDocumentInfo info) {
                return super.convertDocInfoToStrings(info);
            }
        }

        CompareTool compareTool = new TestCompareTool();
        try (PdfReader reader = new PdfReader(inPdf, compareTool.getOutReaderProperties());
                PdfDocument doc = new PdfDocument(reader)) {
            String[] docInfo = compareTool.convertDocInfoToStrings(doc.getDocumentInfo());
            Assertions.assertEquals("very long title to compare later on", docInfo[0]);
            Assertions.assertEquals("itextcore", docInfo[1]);
            Assertions.assertEquals("test file", docInfo[2]);
            Assertions.assertEquals("new job", docInfo[3]);
            Assertions.assertEquals("Adobe Acrobat Pro DC (64-bit) <version>", docInfo[4]);
                }
    }

    @Test
    public void memoryFirstWriterNoFileTest() throws InterruptedException, IOException {
        String firstPdf = destinationFolder + "memoryFirstWriterNoFileTest.pdf";
        String secondPdf = destinationFolder + "memoryFirstWriterNoFileTest2.pdf";
        PdfDocument firstDocument = new PdfDocument(CompareTool.createTestPdfWriter(firstPdf));
        PdfDocument secondDocument = new PdfDocument(CompareTool.createTestPdfWriter(secondPdf));

        PdfPage page1FirstDocument = firstDocument.addNewPage();
        page1FirstDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 400, 50)).setDestination(
                PdfExplicitDestination.createFit(page1FirstDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1FirstDocument.flush();
        firstDocument.close();

        PdfPage page1SecondDocument = secondDocument.addNewPage();
        page1SecondDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 400, 50)).setDestination(
                PdfExplicitDestination.createFit(page1SecondDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1SecondDocument.flush();
        secondDocument.close();

        Assertions.assertNull(new CompareTool().compareByContent(firstPdf, secondPdf, destinationFolder));
        Assertions.assertFalse(new File(firstPdf).exists());
        Assertions.assertFalse(new File(secondPdf).exists());
    }

    @Test
    public void memoryFirstWriterCmpMissingTest() throws IOException {
        String firstPdf = destinationFolder + "memoryFirstWriterCmpMissingTest.pdf";
        String secondPdf = destinationFolder + "cmp_memoryFirstWriterCmpMissingTest.pdf";
        PdfDocument firstDocument = new PdfDocument(CompareTool.createTestPdfWriter(firstPdf));

        PdfPage page1FirstDocument = firstDocument.addNewPage();
        page1FirstDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 400, 50)).setDestination(
                PdfExplicitDestination.createFit(page1FirstDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1FirstDocument.flush();
        firstDocument.close();

        Assertions.assertThrows(IOException.class,
                () -> new CompareTool().compareByContent(firstPdf, secondPdf, destinationFolder));
        Assertions.assertTrue(new File(firstPdf).exists());
    }

    @Test
    public void dumpMemoryFirstWriterOnDiskTest() throws InterruptedException, IOException {
        String firstPdf = destinationFolder + "dumpMemoryFirstWriterOnDiskTest.pdf";
        String secondPdf = destinationFolder + "dumpMemoryFirstWriterOnDiskTest2.pdf";
        PdfDocument firstDocument = new PdfDocument(CompareTool.createTestPdfWriter(firstPdf));
        PdfDocument secondDocument = new PdfDocument(CompareTool.createTestPdfWriter(secondPdf));

        PdfPage page1FirstDocument = firstDocument.addNewPage();
        page1FirstDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 400, 50)).setDestination(
                PdfExplicitDestination.createFit(page1FirstDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1FirstDocument.flush();
        firstDocument.close();

        PdfPage page1SecondDocument = secondDocument.addNewPage();
        page1SecondDocument.addAnnotation(new PdfLinkAnnotation(new Rectangle(100, 560, 260, 25)).setDestination(
                PdfExplicitDestination.createFit(page1SecondDocument)).setBorder(new PdfArray(new float[] {0, 0, 1})));
        page1SecondDocument.flush();
        secondDocument.close();

        Assertions.assertNotNull(new CompareTool().compareByContent(firstPdf, secondPdf, destinationFolder));
        Assertions.assertTrue(new File(firstPdf).exists());
        Assertions.assertTrue(new File(secondPdf).exists());
    }

    @Test
    public void cleanupTest() throws IOException {
        CompareTool.createTestPdfWriter(destinationFolder + "cleanupTest/cleanupTest.pdf");
        Assertions.assertNotNull(MemoryFirstPdfWriter.get(destinationFolder + "cleanupTest/cleanupTest.pdf"));

        Assertions.assertThrows(IllegalArgumentException.class, () -> CompareTool.cleanup(null));

        CompareTool.cleanup(destinationFolder + "cleanupTest");
        Assertions.assertNull(MemoryFirstPdfWriter.get(destinationFolder + "cleanupTest/cleanupTest.pdf"));
    }
}