ContrastAnalyzerTest.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2026 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.contrast;

import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.layer.PdfLayer;
import com.itextpdf.test.ExtendedITextTest;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;


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


    @Test
    public void blackTextOnNoBackGround() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);
        canvas.moveTo(250, 250);
        canvas.beginText();
        canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
        canvas.showText("Test");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        assertEquals(4, results.size());
        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
        dummyDoc.close();
    }

    @Test
    public void whiteTextOnNoBackGround() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
        canvas.showText("Test");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(4, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(1, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }


    @Test
    public void whiteLetterOnBlackBackGroundWhereBackgroundCompletlyCovers() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.rectangle(30, 30, page.getPageSize().getWidth() / 2, page.getPageSize().getHeight() / 2);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void whiteTextOnBlackBackGroundWhereBackgroundCompletlyCovers() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.rectangle(30, 30, page.getPageSize().getWidth() / 2, page.getPageSize().getHeight() / 2);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
        canvas.showText("AT");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(2, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }


    @Test
    public void whiteLetterBlackBackGroundWhereBackgroundHalfCovers() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.rectangle(260, 250, 200, 200);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(2, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
            assertEquals(1, result.getOverlappingAreas().get(1).getContrastRatio(), 0.1);
        }

    }

    @Test
    public void blackLetterNoFillCovers() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        canvas.rectangle(100, 100, 500, 500).clip().endPath();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();
        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21.0, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void textDrawnOutsideOfPageShouldNotBeAnalyzed() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.beginText();
        canvas.moveText(-100, -100); // Position text outside the page
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();
        // No text should be analyzed as it's outside the page
        assertEquals(0, results.size());
    }

    @Test
    @Disabled("DEVSIX-9718: Clipping path handling needs to be improved in the ContrastAnalyzer")
    public void clipTextShouldNotShowUp() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        // Text positioning
        float x = 50;
        float y = 700;

        canvas.saveState();
        canvas.rectangle(50, 690, 200, 60); // clip width cuts text
        canvas.clip();
        canvas.endPath();

        canvas.beginText();

        canvas.setFontAndSize(PdfFontFactory.createFont(), 48);
        canvas.moveText(x, y);
        canvas.showText("1234567890");
        canvas.endText();

        canvas.restoreState();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();
        //8 characters should be fully or partially visible
        // 9 and 0 should be clipped out
        assertEquals(8, results.size());

        for (ContrastResult result : results) {
            //will need to change this as for 8 it's partially visible
            //so there will be 2 contrast ratios
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21.0, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }


    @Test
    public void whiteLetterBlackBackGroundWhereBacgroundDoesNotIntersect() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.rectangle(20, 25, 90, 90);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.GREEN, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(1.37, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }

    }

    @Test
    public void whiteLetter2BackgroundTogetherOverlapSoDefaultBackgroundShouldBeIgnored() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.rectangle(260, 250, 200, 200);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();

        canvas.rectangle(200, 250, 60, 100);
        canvas.setColor(ColorConstants.ORANGE, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 270);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(2, result.getOverlappingAreas().size());
            assertEquals(1.553, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
            assertEquals(21, result.getOverlappingAreas().get(1).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void whiteTextOnOrangegroundWithLayerOn() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        // Create a layer that is initially ON
        PdfLayer layer = new PdfLayer("Background Layer", dummyDoc);
        layer.setOn(true);

        PdfCanvas canvas = new PdfCanvas(page);

        // Draw black rectangle background inside the layer
        canvas.beginLayer(layer);
        canvas.rectangle(30, 30, page.getPageSize().getWidth() / 2, page.getPageSize().getHeight() / 2);
        canvas.setColor(ColorConstants.ORANGE, true);
        canvas.fill();
        canvas.endLayer();

        // Draw white text on top (outside the layer)
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("Test");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        // When layer is ON, the black background should be visible
        assertEquals(4, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(1.5539, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void whiteTextOnOrangeBackGroundWithLayerOff() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        // Create a layer that is initially OFF
        PdfLayer layer = new PdfLayer("Background Layer", dummyDoc);
        layer.setOn(false);

        PdfCanvas canvas = new PdfCanvas(page);

        // Draw black rectangle background inside the layer
        canvas.beginLayer(layer);
        canvas.rectangle(30, 30, page.getPageSize().getWidth() / 2, page.getPageSize().getHeight() / 2);
        canvas.setColor(ColorConstants.ORANGE, true);
        canvas.fill();
        canvas.endLayer();

        // Draw white text on top (outside the layer)
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("Test");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        // When layer is OFF, the black background should still be analyzed
        // because PdfCanvasProcessor processes all content regardless of layer state
        // The layer state only affects viewer display, not the content structure
        assertEquals(4, results.size());

        for (ContrastResult result : results) {
            // The analyzer should still detect the black background even if layer is off
            // because it analyzes the actual PDF content stream
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(1, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void whiteLetterBlackBackgroundCircle() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.circle(300, 300, 100);
        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }


    @Test
    public void whiteLetterBlackBackgroundTriangle() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.moveTo(200, 200);
        canvas.lineTo(400, 200);
        canvas.lineTo(300, 400);
        canvas.closePath();

        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(1, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
        }
    }

    @Test
    public void whiteLetterBlackBackgroundTriangleHalfIntersects() throws IOException {
        PdfDocument dummyDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
        PdfPage page = dummyDoc.addNewPage();

        PdfCanvas canvas = new PdfCanvas(page);

        canvas.moveTo(240, 200);
        canvas.lineTo(400, 200);
        canvas.lineTo(300, 400);
        canvas.closePath();

        canvas.setColor(ColorConstants.BLACK, true);
        canvas.fill();
        canvas.beginText();
        canvas.moveText(250, 250);
        canvas.setColor(ColorConstants.WHITE, true);
        canvas.setFontAndSize(PdfFontFactory.createFont(), 32);
        canvas.showText("T");
        canvas.endText();

        List<ContrastResult> results = new ContrastAnalyzer(true).checkPageContrast(page);
        dummyDoc.close();

        assertEquals(1, results.size());

        for (ContrastResult result : results) {
            assertEquals(2, result.getOverlappingAreas().size());
            assertEquals(21, result.getOverlappingAreas().get(0).getContrastRatio(), 0.1);
            assertEquals(1, result.getOverlappingAreas().get(1).getContrastRatio(), 0.1);
        }
    }
}