StrategyBasedRadialGradientBuilder.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.colors.gradients;

import com.itextpdf.commons.datastructures.Tuple2;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.NoninvertibleTransformException;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;

/**
 * The radial gradient builder with automatic end circle (or ellipse) evaluation for the target filled
 * area based on configured strategy. Start center would be equal to end center, radius would be 0.
 */
public class StrategyBasedRadialGradientBuilder extends AbstractRadialGradientBuilder {

    // center point
    private boolean leftToRight = true;
    private double xOffset = 0.5d;
    private boolean xOffsetRelative = true;
    private boolean bottomToTop = false;
    private double yOffset = 0.5d;
    private boolean yOffsetRelative = true;

    // radius
    // manual radius definition based on target bbox
    private double xRadius = 0d;
    private boolean xRadiusRelative = false;
    private double yRadius = 0d;
    private boolean yRadiusRelative = false;
    // from center based radius definition
    private GradientStrategy gradientStrategy = GradientStrategy.FARTHEST_CORNER;
    private boolean isCircular = false;
    // whether from center based strategy to use or manual
    private boolean isFromCenterStrategy = true;


    /**
     * Constructs the builder instance
     */
    public StrategyBasedRadialGradientBuilder() {
        // empty constructor
    }

    /**
     * Specifies the strategy to determine center point.
     *
     * @param leftToRight     if {@code true} then X offset would be added to the left side X value,
     *                        if {@code false} then X offset would be subtracted from the right side X value
     * @param xOffset         the offset value to add/subtract for X
     * @param xOffsetRelative if {@code true} then X offset treated as relative to bbox width,
     *                        if {@code false} then X offset treated as absolute
     * @param bottomToTop     if {@code true} then Y offset would be subtracted from the top side Y value,
     *                        if {@code false} then Y offset would be added to the bottom side Y value
     * @param yOffset         the offset value to add/subtract for Y
     * @param yOffsetRelative if {@code true} then Y offset treated as relative to bbox height,
     *                        if {@code false} then Y offset treated as absolute
     *
     * @return the current builder instance
     */
    public StrategyBasedRadialGradientBuilder setCenterStrategy(
            boolean leftToRight, double xOffset, boolean xOffsetRelative,
            boolean bottomToTop, double yOffset, boolean yOffsetRelative) {
        this.leftToRight = leftToRight;
        this.xOffset = xOffset;
        this.xOffsetRelative = xOffsetRelative;
        this.bottomToTop = bottomToTop;
        this.yOffset = yOffset;
        this.yOffsetRelative = yOffsetRelative;
        return this;
    }

    /**
     * Set the strategy to calculate radius based on bounding box dimensions.
     *
     * @param xRadius         X radius value
     * @param xRadiusRelative if {@code true} then X radius treated as relative to bbox width,
     *                        if {@code false} then X radius treated as absolute
     * @param yRadius         Y radius value
     * @param yRadiusRelative if {@code true} then Y radius treated as relative to bbox height,
     *                        if {@code false} then Y radius treated as absolute
     *
     * @return the current builder instance
     */
    public StrategyBasedRadialGradientBuilder setRadiusRelativeToBoundingBoxSize(
            double xRadius, boolean xRadiusRelative,
            double yRadius, boolean yRadiusRelative) {
        this.xRadius = xRadius;
        this.xRadiusRelative = xRadiusRelative;
        this.yRadius = yRadius;
        this.yRadiusRelative = yRadiusRelative;

        this.isFromCenterStrategy = false;
        return this;
    }

    /**
     * Set the strategy to predefined one from gradient center.
     *
     * @param isCircular       whether the gradient strategy to be applied for circular or elliptical gradient spreading
     * @param gradientStrategy strategy to be used for calculating target radius from gradient center
     *
     * @return the current builder instance
     */
    public StrategyBasedRadialGradientBuilder setRadiusFromCenterStrategy(boolean isCircular,
            GradientStrategy gradientStrategy) {
        this.isCircular = isCircular;
        this.gradientStrategy = gradientStrategy != null ? gradientStrategy : GradientStrategy.FARTHEST_CORNER;

        this.isFromCenterStrategy = true;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected Tuple2<RadialGradientPoint[], AffineTransform> getGradientVectorWithTransform(
            Rectangle targetBoundingBox, AffineTransform contextTransform) {
        Tuple2<RadialGradientPoint, AffineTransform> coordsWithTransform = evaluateCoordAndTransform(targetBoundingBox);
        RadialGradientPoint target = coordsWithTransform.getFirst();

        RadialGradientPoint[] vector = target == null
                ? null
                : new RadialGradientPoint[] {new RadialGradientPoint(target.getCenter(), 0), target};
        return new Tuple2<RadialGradientPoint[], AffineTransform>(vector, coordsWithTransform.getSecond());
    }

    private Tuple2<RadialGradientPoint, AffineTransform> evaluateCoordAndTransform(Rectangle targetBoundingBox) {
        if (targetBoundingBox == null) {
            return new Tuple2<RadialGradientPoint, AffineTransform>(null, null);
        }
        double cX = evaluateValueOnSegment(targetBoundingBox.getLeft(), targetBoundingBox.getRight(),
                leftToRight, xOffset, xOffsetRelative);
        double cY = evaluateValueOnSegment(targetBoundingBox.getBottom(), targetBoundingBox.getTop(),
                bottomToTop, yOffset, yOffsetRelative);
        Point center = new Point(cX, cY);

        double[] radius = isFromCenterStrategy
                ? getRadiusForCenterBasedStrategy(targetBoundingBox, center)
                : getRadiusForManualStrategy(targetBoundingBox);

        double rX = radius[0];
        double rY = radius[1];
        // if any of radii is 0 or negative, then normalize both to 0
        if (isZero(rX) || isZero(rY)) {
            rX = 0d;
            rY = 0d;
        }

        AffineTransform transform = null;
        if (!isZero(rY - rX)) {
            // ellipse case
            transform = new AffineTransform();
            transform.scale(1.0d, rY / rX);

            try {
                center = transform.inverseTransform(center, null);
            } catch (NoninvertibleTransformException e) {
                throw new PdfException(e.getMessage(), e);
            }
        }

        return new Tuple2<RadialGradientPoint, AffineTransform>(new RadialGradientPoint(center, rX), transform);
    }

    private double[] getRadiusForManualStrategy(Rectangle targetBoundingBox) {
        double rX = evaluateValueOnSegment(0, targetBoundingBox.getWidth(), true, xRadius, xRadiusRelative);
        double rY = evaluateValueOnSegment(0, targetBoundingBox.getHeight(), true, yRadius, yRadiusRelative);

        return new double[] {rX, rY};
    }

    private double[] getRadiusForCenterBasedStrategy(Rectangle targetBoundingBox, Point center) {
        switch (gradientStrategy) {
            case CLOSEST_SIDE:
                return evaluateClosestSideRadius(targetBoundingBox, center);
            case CLOSEST_CORNER:
                return evaluateClosestCornerRadius(targetBoundingBox, center);
            case FARTHEST_SIDE:
                return evaluateFarthestSideRadius(targetBoundingBox, center);
            // default case is equal to FARTHEST_CORNER
            case FARTHEST_CORNER:
            default:
                return evaluateFarthestCornerRadius(targetBoundingBox, center);
        }
    }

    private double[] evaluateClosestSideRadius(Rectangle targetBoundingBox, Point center) {
        double leftDist = Math.abs(center.getX() - targetBoundingBox.getLeft());
        double rightDist = Math.abs(center.getX() - targetBoundingBox.getRight());
        double bottomDist = Math.abs(center.getY() - targetBoundingBox.getBottom());
        double topDist = Math.abs(center.getY() - targetBoundingBox.getTop());

        double horizontalClosest = Math.min(leftDist, rightDist);
        double verticalClosest = Math.min(bottomDist, topDist);

        if (isCircular) {
            double closestDist = Math.min(horizontalClosest, verticalClosest);
            return new double[] {closestDist, closestDist};
        } else {
            return new double[] {horizontalClosest, verticalClosest};
        }
    }

    private double[] evaluateFarthestSideRadius(Rectangle targetBoundingBox, Point center) {
        double leftDist = Math.abs(center.getX() - targetBoundingBox.getLeft());
        double rightDist = Math.abs(center.getX() - targetBoundingBox.getRight());
        double bottomDist = Math.abs(center.getY() - targetBoundingBox.getBottom());
        double topDist = Math.abs(center.getY() - targetBoundingBox.getTop());

        double horizontalFarthest = Math.max(leftDist, rightDist);
        double verticalFarthest = Math.max(bottomDist, topDist);

        if (isCircular) {
            double farthestDist = Math.max(horizontalFarthest, verticalFarthest);
            return new double[] {farthestDist, farthestDist};
        } else {
            return new double[] {horizontalFarthest, verticalFarthest};
        }
    }

    private double[] evaluateClosestCornerRadius(Rectangle targetBoundingBox, Point center) {
        Point[] vertices = targetBoundingBox.toPointsArray();
        Point closestCorner = vertices[0];
        for (int i = 1; i < vertices.length; ++i) {
            if (center.distance(closestCorner) > center.distance(vertices[i])) {
                closestCorner = vertices[i];
            }
        }

        return evaluateRadiusForCorner(center, closestCorner);
    }

    private double[] evaluateFarthestCornerRadius(Rectangle targetBoundingBox, Point center) {
        Point[] vertices = targetBoundingBox.toPointsArray();
        Point farthestCorner = vertices[0];
        for (int i = 1; i < vertices.length; ++i) {
            if (center.distance(farthestCorner) < center.distance(vertices[i])) {
                farthestCorner = vertices[i];
            }
        }

        return evaluateRadiusForCorner(center, farthestCorner);
    }

    private double[] evaluateRadiusForCorner(Point center, Point corner) {
        if (isCircular) {
            double distance = center.distance(corner);
            return new double[] {distance, distance};
        } else {
            double xDiff = Math.abs(corner.getX() - center.getX());
            double yDiff = Math.abs(corner.getY() - center.getY());
            double aspectRatio = yDiff / xDiff;
            double xR = Math.sqrt(xDiff * xDiff + (yDiff / aspectRatio) * (yDiff / aspectRatio));
            double yR = aspectRatio * xR;
            return new double[] {xR, yR};
        }
    }

    private static double evaluateValueOnSegment(double segmentStart, double segmentEnd,
            boolean isFromStart, double offset, boolean offsetRelative) {
        double absoluteOffset = offsetRelative
                ? (segmentEnd - segmentStart) * offset
                : offset;
        return isFromStart
                ? segmentStart + absoluteOffset
                : segmentEnd - absoluteOffset;
    }

    /**
     * Specifies the predefined strategies
     */
    public enum GradientStrategy {
        /**
         * Circle radius equal to the closest side distance from center,
         * ellipse radii equal to the closest horizontal and vertical sides from center.
         */
        CLOSEST_SIDE,
        /**
         * Circle radius equal to the closest corner distance from center,
         * ellipse passes through the closest corner from center
         * while radii aspect ratio is the same as for CLOSEST_SIDE strategy for ellipse.
         */
        CLOSEST_CORNER,
        /**
         * Circle radius equal to the farthest side distance from center,
         * ellipse radii equal to the farthest horizontal and vertical sides from center.
         */
        FARTHEST_SIDE,
        /**
         * Circle radius equal to the farthest corner distance from center,
         * ellipse passes through the farthest corner from center
         * while radii aspect ratio is the same as for FARTHEST_SIDE strategy for ellipse.
         */
        FARTHEST_CORNER
    }
}