GradientColorStop.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.gradients;

import java.util.Arrays;
import java.util.Objects;

/**
 * The gradient stop color structure representing the stop color configuration.
 * The stop color consists of:
 * - {@code float[]} rgb color array. Values should be in [0, 1] range. All values outside of
 * this range would be adjusted to the nearest corner of the range.
 * - {@code double} offset and {@link OffsetType} offset type specifies the coordinate of
 * the stop color on the targeting gradient coordinates vector
 * - {@code double} hint offset and {@link HintOffsetType} hint offset type specifies the color
 * transition mid point offset between the current color and the next color
 */
public class GradientColorStop {
    private final float[] rgb;
    private final float opacity;

    private OffsetType offsetType;
    private double offset;
    private double hintOffset = 0d;
    private HintOffsetType hintOffsetType = HintOffsetType.NONE;

    /**
     * Constructor of stop color with with specified rgb color and default ({@link OffsetType#AUTO})
     * offset
     *
     * @param rgb the color value
     */
    public GradientColorStop(float[] rgb) {
        this(rgb, 1f, 0d, OffsetType.AUTO);
    }

    /**
     * Constructor of stop color with with specified rgb color and offset
     *
     * @param rgb        the color value
     * @param offset     the offset value. Makes sense only if the {@code offsetType} is not {@link OffsetType#AUTO}
     * @param offsetType the offset's type
     */
    public GradientColorStop(float[] rgb, double offset, OffsetType offsetType) {
        this(rgb, 1f, offset, offsetType);
    }

    /**
     * Constructor that creates the stop with the same color as the another stop and new offset
     *
     * @param gradientColorStop the gradient stop color from which the color value would be copied
     * @param offset            the new offset. Makes sense only if the {@code offsetType} is not {@link OffsetType#AUTO}
     * @param offsetType        the new offset's type
     */
    public GradientColorStop(GradientColorStop gradientColorStop, double offset, OffsetType offsetType) {
        this(gradientColorStop.getRgbArray(), gradientColorStop.getOpacity(), offset, offsetType);
    }

    private GradientColorStop(float[] rgb, float opacity, double offset, OffsetType offsetType) {
        this.rgb = copyRgbArray(rgb);

        this.opacity = normalize(opacity);

        setOffset(offset, offsetType);
    }

    /**
     * Get the stop color rgb value
     *
     * @return the copy of stop's rgb value
     */
    public float[] getRgbArray() {
        return copyRgbArray(this.rgb);
    }

    // TODO: DEVSIX-4136 make public with opacity logic implementation
    /**
     * Get the stop color opacity value
     *
     * @return the stop color opacity value
     */
    private float getOpacity() {
        return this.opacity;
    }

    /**
     * Get the offset type
     *
     * @return the offset type
     */
    public OffsetType getOffsetType() {
        return offsetType;
    }

    /**
     * Get the offset value
     *
     * @return the offset value
     */
    public double getOffset() {
        return this.offset;
    }

    /**
     * Get the hint offset value
     *
     * @return the hint offset value
     */
    public double getHintOffset() {
        return hintOffset;
    }

    /**
     * Get the hint offset type
     *
     * @return the hint offset type
     */
    public HintOffsetType getHintOffsetType() {
        return hintOffsetType;
    }

    /**
     * Set the offset specified by its value and type
     *
     * @param offset     the offset's value to be set. Makes sense only if the {@code offsetType}
     *                   is not {@link OffsetType#AUTO}
     * @param offsetType the offset's type to be set
     * @return the current {@link GradientColorStop} instance
     */
    public GradientColorStop setOffset(double offset, OffsetType offsetType) {
        this.offsetType = offsetType != null ? offsetType : OffsetType.AUTO;
        this.offset = this.offsetType != OffsetType.AUTO ? offset : 0d;
        return this;
    }

    /**
     * Set the color hint specified by its value and type ({@link GradientColorStop more details}).
     *
     * @param hintOffset     the hint offset's value to be set. Makes sense only
     *                       if the {@code hintOffsetType} is not {@link HintOffsetType#NONE}
     * @param hintOffsetType the hint offset's type to be set
     * @return the current {@link GradientColorStop} instance
     */
    public GradientColorStop setHint(double hintOffset, HintOffsetType hintOffsetType) {
        this.hintOffsetType = hintOffsetType != null ? hintOffsetType : HintOffsetType.NONE;
        this.hintOffset = this.hintOffsetType != HintOffsetType.NONE ? hintOffset : 0d;
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        GradientColorStop that = (GradientColorStop) o;
        return Float.compare(that.opacity, opacity) == 0 &&
                Double.compare(that.offset, offset) == 0 &&
                Double.compare(that.hintOffset, hintOffset) == 0 &&
                Arrays.equals(rgb, that.rgb) &&
                offsetType == that.offsetType &&
                hintOffsetType == that.hintOffsetType;
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(opacity, offset, hintOffset);
        result = 31 * result + offsetType.hashCode();
        result = 31 * result + hintOffsetType.hashCode();
        result = 31 * result + Arrays.hashCode(rgb);
        return result;
    }

    private static float normalize(float toNormalize) {
        return toNormalize > 1f ? 1f : toNormalize > 0f ? toNormalize : 0f;
    }

    private static float[] copyRgbArray(float[] toCopy) {
        if (toCopy == null || toCopy.length < 3) {
            return new float[] {0f, 0f, 0f};
        }
        return new float[] {normalize(toCopy[0]), normalize(toCopy[1]), normalize(toCopy[2])};
    }

    /**
     * Represents the possible offset type
     */
    public enum OffsetType {
        /**
         * The absolute offset value from the target coordinates vector's start
         */
        ABSOLUTE,
        /**
         * The automatic offset evaluation. The offset value should be evaluated automatically
         * based on the whole stop colors list specified for the gradient. The general auto offset
         * logic should be the next:
         * - find the previous and the next specified offset or hint offset values
         * - the sublist of sequent auto offsets should spread evenly between the found values
         */
        AUTO,
        /**
         * The relative offset value to the target coordinates vector. The {@code 0} value means
         * the target vector start, the {@code 1} value means the target vector end.
         */
        RELATIVE
    }

    /**
     * Represents the possible hint offset type
     */
    public enum HintOffsetType {
        /**
         * The absolute hint offset value on the target gradient value
         */
        ABSOLUTE_ON_GRADIENT,
        /**
         * The relative hint offset value to the target coordinates vector. The {@code 0} value
         * means the target vector start, the {@code 1} value means the target vector end.
         */
        RELATIVE_ON_GRADIENT,
        /**
         * The relative hint offset value to the interval between the current gradient stop color
         * and the next one.
         */
        RELATIVE_BETWEEN_COLORS,
        /**
         * None hint offset specified
         */
        NONE
    }
}