StrategyBasedRadialGradientBuilderTest.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.kernel.colors.ColorConstants;
import com.itextpdf.kernel.colors.gradients.GradientColorStop.OffsetType;
import com.itextpdf.kernel.colors.gradients.StrategyBasedRadialGradientBuilder.GradientStrategy;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;

import java.io.IOException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

/**
 * This test class focuses on different types of radial gradient strategies.
 * Tests related to stop colors work omitted here as they would be equivalent to tests in
 * {@link RadialGradientBuilderTest}.
 */
@Tag("IntegrationTest")
public class StrategyBasedRadialGradientBuilderTest extends ExtendedITextTest {

    private static final String SOURCE_FOLDER =
            "./src/test/resources/com/itextpdf/kernel/colors/gradients/StrategyBasedRadialGradientBuilderTest/";
    private static final String DESTINATION_FOLDER =
            TestUtil.getOutputPath() + "/kernel/colors/gradients/StrategyBasedRadialGradientBuilderTest/";

    @BeforeAll
    public static void beforeClass() {
        createOrClearDestinationFolder(DESTINATION_FOLDER);
    }

    @AfterAll
    public static void afterClass() {
        CompareTool.cleanup(DESTINATION_FOLDER);
    }

    @Test
    public void noSettersTest() {
        Assertions.assertNull(new StrategyBasedRadialGradientBuilder()
                .buildColor(new Rectangle(50f, 450f, 500f, 300f), null, null));
    }

    @Test
    public void noRectangleTest() {
        Assertions.assertNull(new StrategyBasedRadialGradientBuilder()
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE))
                .buildColor(null, null, null));
    }

    @Test
    public void noStrategyProvidedTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("noStrategyProvided.pdf", null, gradientBuilder);
    }

    @Test
    public void absoluteCenterFromLeftBottomTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 120, false, true, 50, false)
                .setRadiusRelativeToBoundingBoxSize(100, false, 100, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("absoluteCenterFromLeftBottom.pdf", null, gradientBuilder);
    }

    @Test
    public void absoluteCenterFromRightTopTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(false, 120, false, false, 50, false)
                .setRadiusRelativeToBoundingBoxSize(100, false, 100, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("absoluteCenterFromRightTop.pdf", null, gradientBuilder);
    }

    @Test
    public void absoluteNegativeCenterFromLeftBottomTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, -120, false, true, -50, false)
                .setRadiusRelativeToBoundingBoxSize(200, false, 200, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("absoluteNegativeCenterFromLeftBottom.pdf", null, gradientBuilder);
    }

    @Test
    public void absoluteNegativeCenterFromRightTopTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(false, -120, false, false, -50, false)
                .setRadiusRelativeToBoundingBoxSize(200, false, 200, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("absoluteNegativeCenterFromRightTop.pdf", null, gradientBuilder);
    }

    @Test
    public void relativeCenterFromLeftBottomTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.1d, true, true, 0.1d, true)
                .setRadiusRelativeToBoundingBoxSize(100, false, 100, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("relativeCenterFromLeftBottom.pdf", null, gradientBuilder);
    }

    @Test
    public void relativeCenterFromRightTopTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(false, 0.1d, true, false, 0.1d, true)
                .setRadiusRelativeToBoundingBoxSize(100, false, 100, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("relativeCenterFromRightTop.pdf", null, gradientBuilder);
    }

    @Test
    public void builderClosestSideEllipseTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(false, GradientStrategy.CLOSEST_SIDE)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderClosestSideEllipse.pdf", null, gradientBuilder);
    }

    @Test
    public void builderClosestCornerEllipseTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(false, GradientStrategy.CLOSEST_CORNER)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderClosestCornerEllipse.pdf", null, gradientBuilder);
    }

    @Test
    public void builderFarthestSideEllipseTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(false, GradientStrategy.FARTHEST_SIDE)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderFarthestSideEllipse.pdf", null, gradientBuilder);
    }

    @Test
    public void builderFarthestCornerEllipseTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(false, GradientStrategy.FARTHEST_CORNER)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderFarthestCornerEllipse.pdf", null, gradientBuilder);
    }

    @Test
    public void builderClosestSideCircleTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(true, GradientStrategy.CLOSEST_SIDE)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderClosestSideCircle.pdf", null, gradientBuilder);
    }

    @Test
    public void builderClosestCornerCircleTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(true, GradientStrategy.CLOSEST_CORNER)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderClosestCornerCircle.pdf", null, gradientBuilder);
    }

    @Test
    public void builderFarthestSideCircleTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(true, GradientStrategy.FARTHEST_SIDE)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderFarthestSideCircle.pdf", null, gradientBuilder);
    }

    @Test
    public void builderFarthestCornerCircleTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setCenterStrategy(true, 0.3, true, true, 0.2, true)
                .setRadiusFromCenterStrategy(true, GradientStrategy.FARTHEST_CORNER)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderFarthestCornerCircle.pdf", null, gradientBuilder);
    }

    @Test
    public void builderZeroManualRadiusWithPadTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setRadiusRelativeToBoundingBoxSize(0, false, 0, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE))
                .setSpread(GradientSpreadMethod.PAD);

        generateAndComparePdfs("builderZeroManualRadiusWithPad.pdf", null, gradientBuilder);
    }

    @Test
    public void builderManualRadiusTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setRadiusRelativeToBoundingBoxSize(0.3d, true, 20, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        generateAndComparePdfs("builderManualRadius.pdf", null, gradientBuilder);
    }

    @Test
    public void builderNegativeRadiusTest() {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .setRadiusRelativeToBoundingBoxSize(-0.3d, true, -20, false)
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE));

        Assertions.assertNull(gradientBuilder.buildColor(
                new Rectangle(50f, 450f, 500f, 300f), null, null));
    }

    @Test
    public void builderWithNoneSpreadingAndCanvasTransformTest() throws IOException, InterruptedException {
        AbstractGradientBuilder<RadialGradientPoint> gradientBuilder = new StrategyBasedRadialGradientBuilder()
                .addStopColor(new GradientColorStop(ColorConstants.RED.getColorValue(), 0d, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.GREEN.getColorValue(), 0.5, OffsetType.RELATIVE))
                .addStopColor(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 1d, OffsetType.RELATIVE))
                .setSpread(GradientSpreadMethod.NONE);

        AffineTransform canvasTransform = AffineTransform.getTranslateInstance(50, -50);
        canvasTransform.scale(0.8, 1.1);
        canvasTransform.rotate(Math.PI / 3, 400f, 550f);
        generateAndComparePdfs("noneSpreadingCanvasTransform.pdf", canvasTransform, gradientBuilder);
    }

    private void generateAndComparePdfs(String fileName, AffineTransform transform,
            AbstractGradientBuilder<RadialGradientPoint> gradientBuilder) throws InterruptedException, IOException {
        String outPdfPath = DESTINATION_FOLDER + fileName;
        try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outPdfPath))) {
            PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage());

            if (transform != null) {
                canvas.concatMatrix(transform);
            }

            Rectangle toDraw = new Rectangle(50f, 450f, 500f, 300f);
            canvas.setFillColor(gradientBuilder.buildColor(toDraw, transform, pdfDoc))
                    .setStrokeColor(ColorConstants.BLACK)
                    .rectangle(toDraw)
                    .fillStroke();
        }

        Assertions.assertNull(new CompareTool()
                .compareByContent(outPdfPath, SOURCE_FOLDER + "cmp_" + fileName, DESTINATION_FOLDER, "diff"));
    }
}