Color.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.colors;

import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.colorspace.PdfCieBasedCs;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;
import com.itextpdf.kernel.pdf.colorspace.PdfDeviceCs;
import com.itextpdf.kernel.pdf.colorspace.PdfDeviceCs.Cmyk;
import com.itextpdf.kernel.pdf.colorspace.PdfDeviceCs.Gray;
import com.itextpdf.kernel.pdf.colorspace.PdfDeviceCs.Rgb;
import com.itextpdf.kernel.pdf.colorspace.PdfSpecialCs;

import java.util.Arrays;

/**
 * Represents a color
 */
public class Color {


    /**
     * The color space of the color
     */
    protected PdfColorSpace colorSpace;

    /**
     * The color value of the color
     */
    protected float[] colorValue;

    /**
     * Creates a Color of certain color space and color value.
     * If color value is set in null, all value components will be initialised with zeroes.
     *
     * @param colorSpace the color space to which the created Color object relates
     * @param colorValue the color value of the created Color object
     */
    protected Color(PdfColorSpace colorSpace, float[] colorValue) {
        this.colorSpace = colorSpace;
        if (colorValue == null) {
            this.colorValue = new float[colorSpace.getNumberOfComponents()];
        } else {
            this.colorValue = colorValue;
        }
    }

    /**
     * Makes a Color of certain color space.
     * All color value components will be initialised with zeroes.
     *
     * @param colorSpace the color space to which the returned Color object relates
     *
     * @return the created Color object.
     */
    public static Color makeColor(PdfColorSpace colorSpace) {
        return makeColor(colorSpace, null);
    }

    /**
     * Makes a Color of certain color space and color value.
     * If color value is set in null, all value components will be initialised with zeroes.
     *
     * @param colorSpace the color space to which the returned Color object relates
     * @param colorValue the color value of the returned Color object
     *
     * @return the created Color object.
     */
    public static Color makeColor(PdfColorSpace colorSpace, float[] colorValue) {
        Color c = null;
        boolean unknownColorSpace = false;
        if (colorSpace instanceof PdfDeviceCs) {
            if (colorSpace instanceof PdfDeviceCs.Gray) {
                c = colorValue != null ? new DeviceGray(colorValue[0]) : new DeviceGray();
            } else if (colorSpace instanceof PdfDeviceCs.Rgb) {
                c = colorValue != null ? new DeviceRgb(colorValue[0], colorValue[1], colorValue[2]) : new DeviceRgb();
            } else if (colorSpace instanceof PdfDeviceCs.Cmyk) {
                c = colorValue != null ? new DeviceCmyk(colorValue[0], colorValue[1], colorValue[2], colorValue[3])
                        : new DeviceCmyk();
            } else {
                unknownColorSpace = true;
            }
        } else if (colorSpace instanceof PdfCieBasedCs) {
            if (colorSpace instanceof PdfCieBasedCs.CalGray) {
                PdfCieBasedCs.CalGray calGray = (PdfCieBasedCs.CalGray) colorSpace;
                c = colorValue != null ? new CalGray(calGray, colorValue[0]) : new CalGray(calGray);
            } else if (colorSpace instanceof PdfCieBasedCs.CalRgb) {
                PdfCieBasedCs.CalRgb calRgb = (PdfCieBasedCs.CalRgb) colorSpace;
                c = colorValue != null ? new CalRgb(calRgb, colorValue) : new CalRgb(calRgb);
            } else if (colorSpace instanceof PdfCieBasedCs.IccBased) {
                PdfCieBasedCs.IccBased iccBased = (PdfCieBasedCs.IccBased) colorSpace;
                c = colorValue != null ? new IccBased(iccBased, colorValue) : new IccBased(iccBased);
            } else if (colorSpace instanceof PdfCieBasedCs.Lab) {
                PdfCieBasedCs.Lab lab = (PdfCieBasedCs.Lab) colorSpace;
                c = colorValue != null ? new Lab(lab, colorValue) : new Lab(lab);
            } else {
                unknownColorSpace = true;
            }
        } else if (colorSpace instanceof PdfSpecialCs) {
            if (colorSpace instanceof PdfSpecialCs.Separation) {
                PdfSpecialCs.Separation separation = (PdfSpecialCs.Separation) colorSpace;
                c = colorValue != null ? new Separation(separation, colorValue[0]) : new Separation(separation);
            } else if (colorSpace instanceof PdfSpecialCs.DeviceN) {
                //NChannel goes here also
                PdfSpecialCs.DeviceN deviceN = (PdfSpecialCs.DeviceN) colorSpace;
                c = colorValue != null ? new DeviceN(deviceN, colorValue) : new DeviceN(deviceN);
            } else if (colorSpace instanceof PdfSpecialCs.Indexed) {
                c = colorValue != null ? new Indexed(colorSpace, (int) colorValue[0]) : new Indexed(colorSpace);
            } else {
                unknownColorSpace = true;
            }
        } else if (colorSpace instanceof PdfSpecialCs.Pattern) {
            c = new Color(colorSpace, colorValue);
        } else {
            unknownColorSpace = true;
        }
        if (unknownColorSpace) {
            throw new PdfException("Unknown color space.");
        }
        return c;
    }

    /**
     * Converts {@link DeviceCmyk DeviceCmyk} color to
     * {@link DeviceRgb DeviceRgb} color
     *
     * @param cmykColor the DeviceCmyk color which will be converted to DeviceRgb color
     *
     * @return converted color
     */
    public static DeviceRgb convertCmykToRgb(DeviceCmyk cmykColor) {
        float cyanComp = 1 - cmykColor.getColorValue()[0];
        float magentaComp = 1 - cmykColor.getColorValue()[1];
        float yellowComp = 1 - cmykColor.getColorValue()[2];
        float blackComp = 1 - cmykColor.getColorValue()[3];

        float r = cyanComp * blackComp;
        float g = magentaComp * blackComp;
        float b = yellowComp * blackComp;
        return new DeviceRgb(r, g, b);
    }

    /**
     * Converts {@link DeviceRgb DeviceRgb} color to
     * {@link DeviceCmyk DeviceCmyk} color
     *
     * @param rgbColor the DeviceRgb color which will be converted to DeviceCmyk color
     *
     * @return converted color
     */
    public static DeviceCmyk convertRgbToCmyk(DeviceRgb rgbColor) {
        float redComp = rgbColor.getColorValue()[0];
        float greenComp = rgbColor.getColorValue()[1];
        float blueComp = rgbColor.getColorValue()[2];

        float k = 1 - Math.max(Math.max(redComp, greenComp), blueComp);
        float c = (1 - redComp - k) / (1 - k);
        float m = (1 - greenComp - k) / (1 - k);
        float y = (1 - blueComp - k) / (1 - k);
        return new DeviceCmyk(c, m, y, k);
    }

    /**
     * Creates a color object based on the passed through values.
     * <p>
     *
     * @param colorValue the float array with the values
     *                   <p>
     *                   The number of array elements determines the colour space in which the colour shall be defined:
     *                   0 - No colour; transparent
     *                   1 - DeviceGray
     *                   3 - DeviceRGB
     *                   4 - DeviceCMYK
     *
     * @return Color the color or null if it's invalid
     */
    public static Color createColorWithColorSpace(float[] colorValue) {
        if (colorValue == null || colorValue.length == 0) {
            return null;
        }
        if (colorValue.length == 1) {
            return makeColor(new Gray(), colorValue);
        }
        if (colorValue.length == 3) {
            return makeColor(new Rgb(), colorValue);
        }
        if (colorValue.length == 4) {
            return makeColor(new Cmyk(), colorValue);
        }
        return null;
    }

    /**
     * Returns the number of color value components
     *
     * @return the number of color value components
     */
    public int getNumberOfComponents() {
        return colorValue.length;
    }

    /**
     * Returns the {@link com.itextpdf.kernel.pdf.colorspace.PdfColorSpace color space}
     * to which the color is related.
     *
     * @return the color space of the color
     */
    public PdfColorSpace getColorSpace() {
        return colorSpace;
    }

    /**
     * Returns the color value of the color
     *
     * @return the color value
     */
    public float[] getColorValue() {
        return colorValue;
    }

    /**
     * Sets the color value of the color
     *
     * @param value new color value
     */
    public void setColorValue(float[] value) {
        if (colorValue.length != value.length) {
            throw new PdfException(KernelExceptionMessageConstant.INCORRECT_NUMBER_OF_COMPONENTS, this);
        }
        colorValue = value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        int result = colorSpace == null ? 0 : colorSpace.getPdfObject().hashCode();
        result = 31 * result + (colorValue != null ? Arrays.hashCode(colorValue) : 0);
        return result;
    }

    /**
     * Indicates whether the color is equal to the given color.
     * The {@link Color#colorSpace color space} and {@link Color#colorValue color value} are considered during the
     * comparison.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Color color = (Color) o;
        return (colorSpace != null ? colorSpace.getPdfObject().equals(color.colorSpace.getPdfObject())
                : color.colorSpace == null) && Arrays.equals(colorValue, color.colorValue);
    }
}