StrategyBasedLinearGradientBuilder.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 com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;

/**
 * The linear gradient builder with automatic coordinates vector evaluation for the target filled
 * area based on configured strategy
 */
public class StrategyBasedLinearGradientBuilder extends AbstractLinearGradientBuilder {

    private double rotateVectorAngle = 0d;
    private GradientStrategy gradientStrategy = GradientStrategy.TO_BOTTOM;
    private boolean isCentralRotationAngleStrategy = false;

    /**
     * Create a new instance of the builder
     */
    public StrategyBasedLinearGradientBuilder() {
    }

    /**
     * Set the strategy to use the minimal coordinates vector that passes through the central point
     * of the target rectangle area, rotated by the specified amount of radians counter clockwise
     * and covers the area to be filled. Zero angle corresponds to the vector from bottom to top.
     *
     * @param radians the radians value to rotate the coordinates vector
     * @return the current builder instance
     */
    public StrategyBasedLinearGradientBuilder setGradientDirectionAsCentralRotationAngle(double radians) {
        this.rotateVectorAngle = radians;
        this.isCentralRotationAngleStrategy = true;
        return this;
    }

    /**
     * Set the strategy to predefined one
     *
     * @param gradientStrategy the strategy to set
     * @return the current builder instance
     */
    public StrategyBasedLinearGradientBuilder setGradientDirectionAsStrategy(GradientStrategy gradientStrategy) {
        this.gradientStrategy = gradientStrategy != null ? gradientStrategy : GradientStrategy.TO_BOTTOM;
        this.isCentralRotationAngleStrategy = false;
        return this;
    }

    /**
     * Get the last set rotate vector angle
     *
     * @return the last set rotate vector angle
     */
    public double getRotateVectorAngle() {
        return rotateVectorAngle;
    }

    /**
     * Get the last set predefined strategy
     *
     * @return the last set predefined strategy
     */
    public GradientStrategy getGradientStrategy() {
        return gradientStrategy;
    }

    /**
     * Is the central rotation angle strategy was set last
     *
     * @return {@code true} if the last strategy that has been set is a custom rotation angle
     */
    public boolean isCentralRotationAngleStrategy() {
        return isCentralRotationAngleStrategy;
    }

    @Override
    protected Point[] getGradientVector(Rectangle targetBoundingBox, AffineTransform contextTransform) {
        if (targetBoundingBox == null) {
            return null;
        }
        return this.isCentralRotationAngleStrategy
                ? buildCentralRotationCoordinates(targetBoundingBox, this.rotateVectorAngle)
                : buildCoordinatesWithGradientStrategy(targetBoundingBox, this.gradientStrategy);
    }

    private static Point[] buildCoordinatesWithGradientStrategy(Rectangle targetBoundingBox,
            GradientStrategy gradientStrategy) {
        double xCenter = targetBoundingBox.getX() + targetBoundingBox.getWidth() / 2;
        double yCenter = targetBoundingBox.getY() + targetBoundingBox.getHeight() / 2;
        switch (gradientStrategy) {
            case TO_TOP:
                return createCoordinates(xCenter, targetBoundingBox.getBottom(), xCenter,
                        targetBoundingBox.getTop());
            case TO_LEFT:
                return createCoordinates(targetBoundingBox.getRight(), yCenter, targetBoundingBox.getLeft(),
                        yCenter);
            case TO_RIGHT:
                return createCoordinates(targetBoundingBox.getLeft(), yCenter, targetBoundingBox.getRight(),
                        yCenter);
            case TO_TOP_LEFT:
                return buildToCornerCoordinates(targetBoundingBox,
                        new Point(targetBoundingBox.getRight(), targetBoundingBox.getTop()));
            case TO_TOP_RIGHT:
                return buildToCornerCoordinates(targetBoundingBox,
                        new Point(targetBoundingBox.getRight(), targetBoundingBox.getBottom()));
            case TO_BOTTOM_RIGHT:
                return buildToCornerCoordinates(targetBoundingBox,
                        new Point(targetBoundingBox.getLeft(), targetBoundingBox.getBottom()));
            case TO_BOTTOM_LEFT:
                return buildToCornerCoordinates(targetBoundingBox,
                        new Point(targetBoundingBox.getLeft(), targetBoundingBox.getTop()));
            // default case is equal to TO_BOTTOM
            case TO_BOTTOM:
            default:
                return createCoordinates(xCenter, targetBoundingBox.getTop(),
                        xCenter, targetBoundingBox.getBottom());
        }
    }

    private static Point[] buildCentralRotationCoordinates(Rectangle targetBoundingBox, double angle) {
        double xCenter = targetBoundingBox.getX() + targetBoundingBox.getWidth() / 2;
        AffineTransform rotateInstance = AffineTransform.getRotateInstance(angle, xCenter,
                targetBoundingBox.getY() + targetBoundingBox.getHeight() / 2);
        return buildCoordinates(targetBoundingBox, rotateInstance);
    }

    private static Point[] buildToCornerCoordinates(Rectangle targetBoundingBox, Point gradientCenterLineRightCorner) {
        AffineTransform transform = buildToCornerTransform(
                new Point(targetBoundingBox.getX() + targetBoundingBox.getWidth() / 2,
                        targetBoundingBox.getY() + targetBoundingBox.getHeight() / 2),
                gradientCenterLineRightCorner);
        return buildCoordinates(targetBoundingBox, transform);
    }

    private static AffineTransform buildToCornerTransform(Point center, Point gradientCenterLineRightCorner) {
        double scale = 1d / (center.distance(gradientCenterLineRightCorner));
        double sin = (gradientCenterLineRightCorner.getY() - center.getY()) * scale;
        double cos = (gradientCenterLineRightCorner.getX() - center.getX()) * scale;
        if (Math.abs(cos) < ZERO_EPSILON) {
            cos = 0d;
            sin = sin > 0d ? 1d : -1d;
        } else if (Math.abs(sin) < ZERO_EPSILON) {
            sin = 0d;
            cos = cos > 0d ? 1d : -1d;
        }
        double m02 = center.getX() * (1d - cos) + center.getY() * sin;
        double m12 = center.getY() * (1d - cos) - center.getX() * sin;
        return new AffineTransform(cos, sin, -sin, cos, m02, m12);
    }

    private static Point[] buildCoordinates(Rectangle targetBoundingBox, AffineTransform transformation) {
        double xCenter = targetBoundingBox.getX() + targetBoundingBox.getWidth() / 2;
        Point start = transformation.transform(new Point(xCenter, targetBoundingBox.getBottom()), null);
        Point end = transformation.transform(new Point(xCenter, targetBoundingBox.getTop()), null);
        Point[] baseVector = new Point[] {start, end};
        double[] targetDomain = evaluateCoveringDomain(baseVector, targetBoundingBox);
        return createCoordinatesForNewDomain(targetDomain, baseVector);
    }

    private static Point[] createCoordinates(double x1, double y1, double x2, double y2) {
        return new Point[] {new Point(x1, y1), new Point(x2, y2)};
    }

    /**
     * Specifies the predefined strategies
     */
    public enum GradientStrategy {
        /**
         * Gradient vector from the middle of the top side to the middle of the bottom side
         */
        TO_BOTTOM,
        /**
         * Evaluates the gradient vector in such way that the first color would be painted
         * at the top right corner, the last one - at the bottom left corner and the middle color
         * line would pass through left corners
         */
        TO_BOTTOM_LEFT,
        /**
         * Evaluates the gradient vector in such way that the first color would be painted
         * at the top left corner, the last one - at the bottom right corner and the middle color
         * line would pass through left corners
         */
        TO_BOTTOM_RIGHT,
        /**
         * Gradient vector from the middle of the right side to the middle of the left side
         */
        TO_LEFT,
        /**
         * Gradient vector from the middle of the left side to the middle of the right side
         */
        TO_RIGHT,
        /**
         * Gradient vector from the middle of the bottom side to the middle of the top side
         */
        TO_TOP,
        /**
         * Evaluates the gradient vector in such way that the first color would be painted
         * at the bottom right corner, the last one - at the top left corner and the middle color
         * line would pass through left corners
         */
        TO_TOP_LEFT,
        /**
         * Evaluates the gradient vector in such way that the first color would be painted
         * at the bottom left corner, the last one - at the top right corner and the middle color
         * line would pass through left corners
         */
        TO_TOP_RIGHT
    }
}