AbstractLinearGradientBuilder.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.geom.AffineTransform;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;
import com.itextpdf.kernel.pdf.colorspace.shading.AbstractPdfShading;
import com.itextpdf.kernel.pdf.colorspace.shading.PdfAxialShading;
import com.itextpdf.kernel.pdf.function.IPdfFunction;
import java.util.List;
/**
* Base class for linear gradient builders implementations.
* <p>
* Color transitions for linear gradients are defined by a series of color stops along a gradient
* vector. A gradient normal defines how the colors in a vector are painted to the surface. For
* a linear gradient, a normal is a line perpendicular to the vector.
* <p>
* Contains the main logic that works with stop colors and creation of the resulted pdf color object.
*/
public abstract class AbstractLinearGradientBuilder extends AbstractGradientBuilder<Point> {
/**
* The epsilon value used for data creation
*
* @deprecated use {@link AbstractGradientBuilder#isZero(double)} instead for zero comparisons
*/
@Deprecated
protected static final double ZERO_EPSILON = 1E-10;
/**
* Adds the new color stop to the end ({@link AbstractLinearGradientBuilder more info}).
*
* <p>
* Note: if the previously added color stop's offset would have grater offset than the added
* one, then the new offset would be normalized to be equal to the previous one. (Comparison
* made between relative on coordinates vector offsets. If any of them has
* the absolute offset, then the absolute value would converted to relative first.)
*
* @param gradientColorStop the gradient stop color to add
*
* @return the current builder instance
*
* @deprecated use {@link AbstractGradientBuilder#addStopColor(GradientColorStop)} instead
*/
@Deprecated
public AbstractLinearGradientBuilder addColorStop(GradientColorStop gradientColorStop) {
super.addStopColor(gradientColorStop);
return this;
}
/**
* Set the spread method to use for the gradient
*
* @param gradientSpreadMethod the gradient spread method to set
*
* @return the current builder instance
*
* @deprecated use {@link AbstractGradientBuilder#setSpread(GradientSpreadMethod)} instead
*/
@Deprecated
public AbstractLinearGradientBuilder setSpreadMethod(GradientSpreadMethod gradientSpreadMethod) {
super.setSpread(gradientSpreadMethod);
return this;
}
/**
* Get the copy of current color stops list. Note that the stop colors are not copied here
*
* @return the copy of current stop colors list
*
* @deprecated use {@link AbstractGradientBuilder#getStopColors()} instead
*/
@Deprecated
public List<GradientColorStop> getColorStops() {
return super.getStopColors();
}
/**
* Get the current spread method
*
* @return the current spread method
*
* @deprecated use {@link AbstractGradientBuilder#getSpread()} instead
*/
@Deprecated
public GradientSpreadMethod getSpreadMethod() {
return super.getSpread();
}
/**
* Returns the base gradient vector in gradient vector space. This vector would be set
* as shading coordinates vector and its length would be used to translate all color stops
* absolute offsets into the relatives.
*
* @param targetBoundingBox the rectangle to be covered by constructed color in current space
* @param contextTransform the current canvas transformation
*
* @return the array of exactly two elements specifying the gradient coordinates vector
*
* @deprecated use {@link AbstractGradientBuilder#getGradientVectorWithTransform(Rectangle, AffineTransform)}
*/
@Deprecated
protected abstract Point[] getGradientVector(Rectangle targetBoundingBox,
AffineTransform contextTransform);
/**
* Returns the current space to gradient vector space transformations that should be applied
* to the shading color. The transformation should be invertible as the current target
* bounding box coordinates should be transformed into the resulted shading space coordinates.
*
* @param targetBoundingBox the rectangle to be covered by constructed color in current space
* @param contextTransform the current canvas transformation
*
* @return the additional transformation to be concatenated to the current for resulted shading
* or {@code null} if no additional transformation is specified
*
* @deprecated use {@link AbstractGradientBuilder#getGradientVectorWithTransform(Rectangle, AffineTransform)}
*/
@Deprecated
protected AffineTransform getCurrentSpaceToGradientVectorSpaceTransformation(
Rectangle targetBoundingBox, AffineTransform contextTransform) {
return null;
}
/**
* Evaluates the minimal domain that covers the box with vector normals.
* The domain corresponding to the initial vector is [0, 1].
*
* @param coords the array of exactly two elements that describe
* the base vector (corresponding to [0,1] domain, that need to be adjusted
* to cover the box
* @param toCover the box that needs to be covered
*
* @return the array of two elements in ascending order specifying the calculated covering
* domain
*/
// TODO: DEVSIX-8808 move the implementation directly into the place where we call this method
protected static double[] evaluateCoveringDomain(Point[] coords, Rectangle toCover) {
if (toCover == null) {
return new double[] {0d, 1d};
}
double scale = 1d / (coords[0].distance(coords[1]));
AffineTransform transform = getToIntervalTransform(coords[0], coords[1], scale);
Point[] rectanglePoints = toCover.toPointsArray();
double minX = transform.transform(rectanglePoints[0], null).getX();
double maxX = minX;
for (int i = 1; i < rectanglePoints.length; ++i) {
double currentX = transform.transform(rectanglePoints[i], null).getX();
minX = Math.min(minX, currentX);
maxX = Math.max(maxX, currentX);
}
return new double[] {minX, maxX};
}
/**
* Expand the base vector to cover the new domain
*
* @param newDomain the array of exactly two elements that specifies the domain
* that should be covered by the created vector
* @param baseVector the array of exactly two elements that specifies the base vector
* which corresponds to [0, 1] domain
*
* @return the array of two
*/
// TODO: DEVSIX-8808 move the implementation directly into the place where we call this method
protected static Point[] createCoordinatesForNewDomain(double[] newDomain, Point[] baseVector) {
double xDiff = baseVector[1].getX() - baseVector[0].getX();
double yDiff = baseVector[1].getY() - baseVector[0].getY();
Point[] targetCoords = new Point[] {
baseVector[0].getLocation(),
baseVector[1].getLocation()
};
targetCoords[0].move(xDiff * newDomain[0], yDiff * newDomain[0]);
targetCoords[1].move(xDiff * (newDomain[1] - 1), yDiff * (newDomain[1] - 1));
return targetCoords;
}
/**
* {@inheritDoc}
*/
@Override
protected double[] computeCoveringDomain(Point[] coords, Rectangle toCover) {
return evaluateCoveringDomain(coords, toCover);
}
/**
* {@inheritDoc}
*/
@Override
protected Point[] createCoordsForNewDomain(double[] newDomain, Point[] baseVector) {
return createCoordinatesForNewDomain(newDomain, baseVector);
}
/**
* {@inheritDoc}
*/
@Override
protected Point[] createCoveringCoordinates(Rectangle targetBoundingBox) {
return new Point[] {new Point(targetBoundingBox.getLeft(), targetBoundingBox.getBottom()),
new Point(targetBoundingBox.getRight(), targetBoundingBox.getBottom())};
}
/**
* {@inheritDoc}
*/
@Override
protected double getBaseVectorLength(Point[] coordinates) {
return coordinates[1].distance(coordinates[0]);
}
/**
* {@inheritDoc}
*/
@Override
protected PdfArray createCoordsDictEntry(Point[] coordinates) {
assert coordinates != null && coordinates.length == 2;
return new PdfArray(new double[] {coordinates[0].getX(), coordinates[0].getY(),
coordinates[1].getX(), coordinates[1].getY()});
}
/**
* {@inheritDoc}
*/
@Override
protected AbstractPdfShading createPdfShading(
PdfColorSpace colorSpace, PdfArray coordinates,
PdfArray coordinatesDomain, IPdfFunction stopsFunction) {
return new PdfAxialShading(colorSpace, coordinates, coordinatesDomain, stopsFunction);
}
}