ColorContrastCalculator.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.DeviceRgb;

/**
 * Utility class for calculating color contrast ratios according to the Web Content Accessibility Guidelines (WCAG) 2.1.
 * <p>
 * The contrast ratio ranges from 1:1 (no contrast) to 21:1 (maximum contrast between black and white).
 */
public final class ColorContrastCalculator {

    private static final double LUMINANCE_OFFSET = 0.05;
    private static final double SRGB_LINEARIZATION_THRESHOLD = 0.04045;
    private static final double SRGB_LINEARIZATION_DIVISOR = 12.92;
    private static final double SRGB_LINEARIZATION_COEFFICIENT = 1.055;
    private static final double SRGB_LINEARIZATION_OFFSET = 0.055;
    private static final double SRGB_LINEARIZATION_EXPONENT = 2.4;

    // ITU-R BT.709 coefficients for relative luminance calculation
    private static final double RED_LUMINANCE_COEFFICIENT = 0.2126;
    private static final double GREEN_LUMINANCE_COEFFICIENT = 0.7152;
    private static final double BLUE_LUMINANCE_COEFFICIENT = 0.0722;

    private static final int RED_COMPONENT_INDEX = 0;
    private static final int GREEN_COMPONENT_INDEX = 1;
    private static final int BLUE_COMPONENT_INDEX = 2;

    /**
     * Private constructor to prevent instantiation of this utility class.
     */
    private ColorContrastCalculator() {
        // Utility class
    }

    /**
     * Calculates the contrast ratio between two colors according to WCAG 2.1 guidelines.
     * <p>
     * The contrast ratio is calculated as (L1 + 0.05) / (L2 + 0.05), where L1 is the relative
     * luminance of the lighter color and L2 is the relative luminance of the darker color.
     * <p>
     * The resulting value ranges from 1:1 (identical colors) to 21:1 (black and white).
     *
     * @param color1 the first color to compare, must not be {@code null}
     * @param color2 the second color to compare, must not be {@code null}
     *
     * @return the contrast ratio between the two colors, ranging from 1.0 to 21.0
     *
     * @throws IllegalArgumentException if either color is {@code null}
     */
    public static double contrastRatio(DeviceRgb color1, DeviceRgb color2) {
        if (color1 == null || color2 == null) {
            throw new IllegalArgumentException("Colors must not be null");
        }

        double[] components1 = extractRgbComponents(color1);
        double[] components2 = extractRgbComponents(color2);

        return contrastRatio(
                components1[RED_COMPONENT_INDEX], components1[GREEN_COMPONENT_INDEX], components1[BLUE_COMPONENT_INDEX],
                components2[RED_COMPONENT_INDEX], components2[GREEN_COMPONENT_INDEX],
                components2[BLUE_COMPONENT_INDEX]);
    }

    /**
     * Calculates the contrast ratio between two RGB colors according to WCAG 2.1 guidelines.
     * <p>
     * The contrast ratio is calculated as (L1 + 0.05) / (L2 + 0.05), where L1 is the relative
     * luminance of the lighter color and L2 is the relative luminance of the darker color.
     *
     * @param r1 red component of the first color (0-1)
     * @param g1 green component of the first color (0-1)
     * @param b1 blue component of the first color (0-1)
     * @param r2 red component of the second color (0-1)
     * @param g2 green component of the second color (0-1)
     * @param b2 blue component of the second color (0-1)
     *
     * @return the contrast ratio between the two colors, ranging from 1.0 to 21.0
     */
    public static double contrastRatio(
            double r1, double g1, double b1,
            double r2, double g2, double b2) {
        double l1 = luminance(r1, g1, b1);
        double l2 = luminance(r2, g2, b2);
        double lighter = Math.max(l1, l2);
        double darker = Math.min(l1, l2);
        return (lighter + LUMINANCE_OFFSET) / (darker + LUMINANCE_OFFSET);
    }

    /**
     * Extracts the RGB components from a DeviceRgb color.
     *
     * @param color the color to extract components from
     *
     * @return an array containing the red, green, and blue components (0-1)
     */
    private static double[] extractRgbComponents(DeviceRgb color) {
        float[] colorValues = color.getColorValue();
        return new double[] {
                clampToRgbRange(colorValues[RED_COMPONENT_INDEX]),
                clampToRgbRange(colorValues[GREEN_COMPONENT_INDEX]),
                clampToRgbRange(colorValues[BLUE_COMPONENT_INDEX])
        };
    }

    /**
     * Clamps an integer value to the valid RGB range of 0-255.
     *
     * @param value the value to clamp
     *
     * @return the clamped value, guaranteed to be between 0 and 1 inclusive
     */
    private static double clampToRgbRange(float value) {
        if (value < 0) {
            return 0;
        }
        if (value > 1) {
            return 1;
        }
        return value;
    }

    /**
     * Converts an sRGB color channel value to linear RGB.
     * <p>
     * This implements the inverse of the sRGB gamma correction according to the sRGB specification.
     * Values below the threshold use a linear transformation, while values above use a power function.
     *
     * @param channel the color channel value in the range 0-1
     *
     * @return the linearized channel value in the range 0.0-1.0
     */
    private static double linearize(double channel) {
        return (channel <= SRGB_LINEARIZATION_THRESHOLD)
                ? (channel / SRGB_LINEARIZATION_DIVISOR)
                : Math.pow((channel + SRGB_LINEARIZATION_OFFSET) / SRGB_LINEARIZATION_COEFFICIENT,
                        SRGB_LINEARIZATION_EXPONENT);
    }

    /**
     * Calculates the relative luminance of an RGB color according to WCAG 2.1.
     * <p>
     * The relative luminance is calculated using the ITU-R BT.709 coefficients:
     * L = 0.2126 * R + 0.7152 * G + 0.0722 * B, where R, G, and B are the linearized color components.
     *
     * @param r red component (0-1)
     * @param g green component (0-1)
     * @param b blue component (0-1)
     *
     * @return the relative luminance in the range 0.0 (black) to 1.0 (white)
     */
    private static double luminance(double r, double g, double b) {
        double rLin = linearize(r);
        double gLin = linearize(g);
        double bLin = linearize(b);
        return RED_LUMINANCE_COEFFICIENT * rLin
                + GREEN_LUMINANCE_COEFFICIENT * gLin
                + BLUE_LUMINANCE_COEFFICIENT * bLin;
    }

}