ColorContrastCheckerTest.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.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
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.validation.IValidationContext;
import com.itextpdf.kernel.validation.ValidationContainer;
import com.itextpdf.kernel.validation.ValidationType;
import com.itextpdf.kernel.validation.context.PdfPageValidationContext;
import com.itextpdf.test.AssertUtil;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.LogLevelConstants;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@Tag("IntegrationTest")
public class ColorContrastCheckerTest extends ExtendedITextTest {
@Test
public void testSetMinimalPercentualCoverageValid() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
AssertUtil.doesNotThrow(() -> {
checker.setMinimalPercentualCoverage(0.5);
});
}
@Test
public void testSetMinimalPercentualCoverageZero() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
AssertUtil.doesNotThrow(() -> {
checker.setMinimalPercentualCoverage(0.0);
});
}
@Test
public void testSetMinimalPercentualCoverageOne() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
AssertUtil.doesNotThrow(() -> {
checker.setMinimalPercentualCoverage(1.0);
});
}
@Test
public void testSetMinimalPercentualCoverageNegative() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
Exception exception = assertThrows(IllegalArgumentException.class,
() -> checker.setMinimalPercentualCoverage(-0.1));
assertEquals("Minimal percentual coverage must be a value between 0.0 and 1.0", exception.getMessage());
}
@Test
public void testSetMinimalPercentualCoverageGreaterThanOne() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
Exception exception = assertThrows(IllegalArgumentException.class,
() -> checker.setMinimalPercentualCoverage(1.1));
assertEquals("Minimal percentual coverage must be a value between 0.0 and 1.0", exception.getMessage());
}
@Test
public void testSetCheckWcagAATrue() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
ColorContrastChecker result = checker.setCheckWcagAA(true);
assertSame(checker, result);
}
@Test
public void testSetCheckWcagAAFalse() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
ColorContrastChecker result = checker.setCheckWcagAA(false);
assertSame(checker, result);
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate =
KernelLogMessageConstant.BOTH_WCAG_AA_AND_AAA_COMPLIANCE_CHECKS_DISABLED, logLevel =
LogLevelConstants.WARN))
public void testSetCheckWcagAAFalseLogsWarningWhenBothDisabled() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
AssertUtil.doesNotThrow(() -> {
checker.setCheckWcagAAA(false);
checker.setCheckWcagAA(false);
});
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate =
KernelLogMessageConstant.BOTH_WCAG_AA_AND_AAA_COMPLIANCE_CHECKS_DISABLED, logLevel =
LogLevelConstants.WARN))
public void testSetCheckWcagAAAFalseLogsWarningWhenBothDisabled() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
AssertUtil.doesNotThrow(() -> {
checker.setCheckWcagAAA(false);
checker.setCheckWcagAA(false);
});
}
@Test
public void testIsPdfObjectReadyToFlush() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
assertTrue(checker.isPdfObjectReadyToFlush(null));
}
@Test
public void testValidateWithNonPdfPageContext() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
IValidationContext context = new IValidationContext() {
@Override
public ValidationType getType() {
return ValidationType.PDF_DOCUMENT;
}
};
AssertUtil.doesNotThrow(() -> {
checker.validate(context);
});
}
@Test
public void testValidateWithCompliantBlackTextOnWhiteBackground() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception - black on white is compliant
AssertUtil.doesNotThrow(() -> {
checker.validate(context);
});
pdfDoc.close();
}
@Test
public void testValidateWithNonCompliantTextThrowsException() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create low contrast: light gray text on white background
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
Exception exception = assertThrows(PdfException.class, () -> checker.validate(context));
assertTrue(exception.getMessage().contains("Color contrast check failed"));
assertTrue(exception.getMessage().contains("Page 1"));
pdfDoc.close();
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = "Page 1: Text: 'T', ", logLevel = LogLevelConstants.WARN))
public void testValidateWithNonCompliantTextLogsWarning() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create low contrast: light gray text on white background
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("T");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(true, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should log warning but not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithWcagAAOnlyEnabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
checker.setCheckWcagAAA(false);
checker.setCheckWcagAA(true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithWcagAAAOnlyEnabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
checker.setCheckWcagAA(false);
checker.setCheckWcagAAA(true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate =
KernelLogMessageConstant.BOTH_WCAG_AA_AND_AAA_COMPLIANCE_CHECKS_DISABLED, logLevel =
LogLevelConstants.WARN))
public void testValidateWithBothWcagChecksDisabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create low contrast text
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.CYAN, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, true);
checker.setCheckWcagAA(false);
checker.setCheckWcagAAA(false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception because checks are disabled
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithIndividualGlyphsEnabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("ABC");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(true, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithLargeText() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Large text (20pt) has different WCAG requirements
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 20);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithTextOnColoredBackground() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Draw colored background
PdfCanvas canvas = new PdfCanvas(page);
canvas.setFillColor(ColorConstants.BLUE);
canvas.rectangle(50, 50, 200, 100);
canvas.fill();
// Draw white text on blue background
canvas.beginText();
canvas.setColor(ColorConstants.WHITE, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception - white on blue should be compliant
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithMinimalCoverageFiltering() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
checker.setMinimalPercentualCoverage(0.9); // Very high threshold
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testMethodChaining() {
ColorContrastChecker checker = new ColorContrastChecker(false, false);
ColorContrastChecker result = checker
.setCheckWcagAA(true)
.setCheckWcagAAA(false);
assertSame(checker, result);
}
@Test
public void testValidateWithTextWithoutParent() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create text without parent context
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("X");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(true, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithAACompliantButNotAAA() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create a scenario where AA passes but AAA fails
// Using gray on white which has ~4.5:1 ratio
PdfCanvas canvas = new PdfCanvas(page);
canvas.setFillColor(ColorConstants.LIGHT_GRAY);
canvas.rectangle(50, 50, 200, 100);
canvas.fill();
canvas.beginText();
canvas.setColor(ColorConstants.BLACK, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 80);
canvas.showText("Test");
canvas.endText();
// Enable only AAA check
ColorContrastChecker checker = new ColorContrastChecker(false, false);
checker.setCheckWcagAA(false);
checker.setCheckWcagAAA(true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception with logging mode
AssertUtil.doesNotThrow(() -> {
checker.validate(context);
});
pdfDoc.close();
}
@Test
public void testValidateWithMultipleBackgrounds() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Draw multiple overlapping backgrounds
PdfCanvas canvas = new PdfCanvas(page);
// First background
canvas.setFillColor(ColorConstants.RED);
canvas.rectangle(50, 50, 150, 100);
canvas.fill();
// Second overlapping background
canvas.setFillColor(ColorConstants.BLUE);
canvas.rectangle(100, 50, 150, 100);
canvas.fill();
// Draw text over overlapping area
canvas.beginText();
canvas.setColor(ColorConstants.WHITE, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(120, 80);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithSmallFontAndLowContrast() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Small font with insufficient contrast
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 8); // Small font
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should throw exception for low contrast with small font
assertThrows(PdfException.class, () -> checker.validate(context));
pdfDoc.close();
}
@Test
public void testValidateWithLargeFontAndModerateContrast() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Large font (>18pt) has more lenient requirements
PdfCanvas canvas = new PdfCanvas(page);
// Background
canvas.setFillColor(ColorConstants.LIGHT_GRAY);
canvas.rectangle(50, 50, 200, 100);
canvas.fill();
// Large text
canvas.beginText();
canvas.setColor(ColorConstants.BLACK, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 24);
canvas.moveText(100, 80);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw exception - large text has lower requirements
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateWithZeroCoverageThreshold() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, false);
checker.setMinimalPercentualCoverage(0.0); // Include all backgrounds
PdfPageValidationContext context = new PdfPageValidationContext(page);
checker.validate(context);
pdfDoc.close();
}
@Test
public void testValidateMessageIncludesParentText() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create low contrast text that will have parent context
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(true, true);
PdfPageValidationContext context = new PdfPageValidationContext(page);
Exception e = Assertions.assertThrows(PdfException.class, () -> checker.validate(context));
System.out.println(e.getMessage());
Assertions.assertTrue(e.getMessage().contains("parent text: 'Test'"));
}
@Test
public void testValidateAAFailsButAAADisabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create text that fails AA but AAA is disabled
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
ColorContrastChecker checker = new ColorContrastChecker(false, true);
checker.setCheckWcagAAA(false); // Disable AAA
checker.setCheckWcagAA(true); // Enable AA
PdfPageValidationContext context = new PdfPageValidationContext(page);
Exception exception = assertThrows(PdfException.class, () -> checker.validate(context));
assertTrue(exception.getMessage().contains("WCAG AA compliant"));
assertFalse(exception.getMessage().contains("WCAG AAA compliant"));
pdfDoc.close();
}
@Test
public void testValidateAAAFailsButAADisabled() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
ValidationContainer container = new ValidationContainer();
ColorContrastChecker contrastChecker = new ColorContrastChecker(false, true);
contrastChecker.setCheckWcagAA(false);
contrastChecker.setCheckWcagAAA(true);
container.addChecker(contrastChecker);
pdfDoc.getDiContainer().register(ValidationContainer.class, container);
PdfPage page = pdfDoc.addNewPage();
// Create text that passes AA but fails AAA
PdfCanvas canvas = new PdfCanvas(page);
canvas.setFillColor(ColorConstants.LIGHT_GRAY);
canvas.rectangle(50, 50, 200, 100);
canvas.fill();
canvas.beginText();
canvas.setColor(ColorConstants.CYAN, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 80);
canvas.showText("Test");
canvas.endText();
PdfPageValidationContext context = new PdfPageValidationContext(page);
Exception exception = assertThrows(PdfException.class, () -> pdfDoc.close());
assertTrue(exception.getMessage().contains("WCAG AAA compliant"));
assertFalse(exception.getMessage().contains("WCAG AA compliant"));
}
@Test
public void testValidateBothAAAndAAAFail() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
ValidationContainer container = new ValidationContainer();
container.addChecker(new ColorContrastChecker(false, true));
pdfDoc.getDiContainer().register(ValidationContainer.class, container);
PdfPage page = pdfDoc.addNewPage();
// Create very low contrast that fails both
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setColor(ColorConstants.LIGHT_GRAY, true);
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
Exception exception = assertThrows(PdfException.class, () -> pdfDoc.close());
assertTrue(exception.getMessage().contains("WCAG AA compliant"));
assertTrue(exception.getMessage().contains("WCAG AAA compliant"));
}
@Test
public void testConstructorSetsDefaultValues() throws IOException {
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()));
PdfPage page = pdfDoc.addNewPage();
// Create compliant text
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText();
canvas.setFontAndSize(PdfFontFactory.createFont(), 12);
canvas.moveText(100, 100);
canvas.showText("Test");
canvas.endText();
// Constructor should set both WCAG checks to true by default
ColorContrastChecker checker = new ColorContrastChecker(false, false);
PdfPageValidationContext context = new PdfPageValidationContext(page);
// Should not throw - defaults allow compliant text
checker.validate(context);
pdfDoc.close();
}
}