AbstractGradientBuilder.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.commons.utils.MessageFormatUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.colors.Color;
import com.itextpdf.kernel.colors.PatternColor;
import com.itextpdf.kernel.colors.gradients.GradientColorStop.HintOffsetType;
import com.itextpdf.kernel.colors.gradients.GradientColorStop.OffsetType;
import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.NoninvertibleTransformException;
import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.colorspace.PdfColorSpace;
import com.itextpdf.kernel.pdf.colorspace.PdfDeviceCs;
import com.itextpdf.kernel.pdf.colorspace.PdfPattern;
import com.itextpdf.kernel.pdf.colorspace.shading.AbstractPdfShading;
import com.itextpdf.kernel.pdf.function.AbstractPdfFunction;
import com.itextpdf.kernel.pdf.function.IPdfFunction;
import com.itextpdf.kernel.pdf.function.PdfType2Function;
import com.itextpdf.kernel.pdf.function.PdfType3Function;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.slf4j.LoggerFactory;
/**
* Base class for gradient builders implementations.
*/
public abstract class AbstractGradientBuilder<T> {
/**
* The epsilon value used for data creation
*/
private static final double ZERO_EPSILON = 1E-10;
private final List<GradientColorStop> stops = new ArrayList<>();
private GradientSpreadMethod spreadMethod = GradientSpreadMethod.NONE;
/**
* Adds the new color stop to the end ({@link AbstractGradientBuilder 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 be converted to relative first.)
*
* @param gradientColorStop the gradient stop color to add
*
* @return the current builder instance
*/
public AbstractGradientBuilder<T> addStopColor(GradientColorStop gradientColorStop) {
if (gradientColorStop != null) {
this.stops.add(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
*/
public AbstractGradientBuilder<T> setSpread(GradientSpreadMethod gradientSpreadMethod) {
if (gradientSpreadMethod != null) {
this.spreadMethod = gradientSpreadMethod;
} else {
this.spreadMethod = GradientSpreadMethod.NONE;
}
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
*/
public List<GradientColorStop> getStopColors() {
return new ArrayList<>(this.stops);
}
/**
* Get the current spread method
*
* @return the current spread method
*/
public GradientSpreadMethod getSpread() {
return this.spreadMethod;
}
/**
* Builds the {@link Color} object representing the gradient with specified configuration
* that fills the target bounding box.
*
* @param targetBoundingBox the bounding box to be filled in current space
* @param contextTransform the transformation from the base coordinates space into
* the current space. The {@code null} value is valid and can be used
* if there is no transformation from base coordinates to current space
* specified, or it is equal to identity transformation.
* @param document the {@link PdfDocument} for which the linear gradient would be built.
*
* @return the constructed {@link Color} or {@code null} if no color to be applied
* or base gradient vector has been specified
*/
// TODO: DEVSIX-4136 the document argument would be required for opaque gradients
// (as we would need to create a mask form xObject)
public Color buildColor(Rectangle targetBoundingBox, AffineTransform contextTransform, PdfDocument document) {
Tuple2<T[], AffineTransform> gradientVectorWithTransform = getGradientVectorWithTransform(
targetBoundingBox, contextTransform);
T[] baseCoordinatesVector = gradientVectorWithTransform == null ? null : gradientVectorWithTransform.getFirst();
if (baseCoordinatesVector == null || this.stops.isEmpty()) {
// Can not create gradient color with 0 stops or null coordinates vector
return null;
}
// evaluate actual coordinates and transformation
AffineTransform shadingTransform = new AffineTransform();
if (contextTransform != null) {
shadingTransform.concatenate(contextTransform);
}
AffineTransform gradientTransformation = gradientVectorWithTransform.getSecond();
if (gradientTransformation != null) {
try {
if (targetBoundingBox != null) {
targetBoundingBox = Rectangle.calculateBBox(Arrays.asList(
gradientTransformation.inverseTransform(
new Point(targetBoundingBox.getLeft(), targetBoundingBox.getBottom()), null),
gradientTransformation.inverseTransform(
new Point(targetBoundingBox.getLeft(), targetBoundingBox.getTop()), null),
gradientTransformation.inverseTransform(
new Point(targetBoundingBox.getRight(), targetBoundingBox.getBottom()), null),
gradientTransformation.inverseTransform(
new Point(targetBoundingBox.getRight(), targetBoundingBox.getTop()), null)
));
}
shadingTransform.concatenate(gradientTransformation);
} catch (NoninvertibleTransformException e) {
LoggerFactory.getLogger(getClass())
.error(IoLogMessageConstant.UNABLE_TO_INVERT_GRADIENT_TRANSFORMATION);
}
}
AbstractPdfShading pdfShading = constructPdfShading(baseCoordinatesVector, this.stops, this.spreadMethod,
targetBoundingBox, document);
if (pdfShading == null) {
return null;
}
PdfPattern.Shading shading = new PdfPattern.Shading(pdfShading);
if (!shadingTransform.isIdentity()) {
double[] matrix = new double[6];
shadingTransform.getMatrix(matrix);
shading.setMatrix(new PdfArray(matrix));
}
return new PatternColor(shading);
}
/**
* Returns the base gradient vector in gradient vector space and the current space
* to gradient vector space transformations that should be applied to the shading color.
*
* <p>
* The vector would be set as shading coordinates vector and its length would be used
* to translate all color stops absolute offsets into the relatives.
*
* <p>
* 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 pair of the vector, which is an array of exactly two elements specifying
* the gradient coordinates vector, and the additional transformation to be concatenated to
* the current for resulted shading or {@code null} if no additional transformation is specified
*/
// TODO: DEVSIX-8808 make this method abstract
// (default implementation was added for avoid breaking changes in AbstractLinearGradientBuilder)
protected Tuple2<T[], AffineTransform> getGradientVectorWithTransform(Rectangle targetBoundingBox,
AffineTransform contextTransform) {
return null;
}
/**
* Computes 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
*/
protected abstract double[] computeCoveringDomain(T[] coords, Rectangle toCover);
/**
* 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
*/
protected abstract T[] createCoordsForNewDomain(double[] newDomain, T[] baseVector);
/**
* Creates some gradient vector which would cover the target bounding box.
*
* @param targetBoundingBox the bounding box to cover
*
* @return constructed gradient vector
*/
protected abstract T[] createCoveringCoordinates(Rectangle targetBoundingBox);
/**
* Evaluates base vector length for future color stops spreading.
*
* @param coordinates the base vector
*
* @return base vector length
*/
protected abstract double getBaseVectorLength(T[] coordinates);
/**
* Converts gradient vector into pdf coordinates array for shading.
*
* @param coordinates vector to convert
*
* @return constructed pdf array
*/
protected abstract PdfArray createCoordsDictEntry(T[] coordinates);
/**
* Creates the gradient shading object.
*
* @param colorSpace colorspace to use
* @param coordinates gradient coordinates
* @param coordinatesDomain gradient coordinates domain
* @param stopsFunction color stops function
*
* @return constructed gradient shading
*/
protected abstract AbstractPdfShading createPdfShading(
PdfColorSpace colorSpace, PdfArray coordinates,
PdfArray coordinatesDomain, IPdfFunction stopsFunction);
/**
* Constructs {@link AffineTransform} which represents transformation from current coordinates
* into the coordinates in which domainStart would be (0,0), domain end would be on X axes
* (i.e. (X,0) coordinate) and the surface scaled both on X and Y by {@code scale} factor.
*
* @param domainStart the point to become (0,0)
* @param domainEnd the point to be placed on X axes
* @param scale the scale factor
*
* @return the constructed transformation
*/
protected static AffineTransform getToIntervalTransform(Point domainStart, Point domainEnd, double scale) {
AffineTransform transform = new AffineTransform();
double sin = -(domainEnd.getY() - domainStart.getY()) * scale;
double cos = (domainEnd.getX() - domainStart.getX()) * scale;
if (isZero(cos)) {
cos = 0d;
sin = sin > 0d ? 1d : -1d;
} else if (isZero(sin)) {
sin = 0d;
cos = cos > 0d ? 1d : -1d;
}
transform.concatenate(new AffineTransform(cos, sin, -sin, cos, 0, 0));
transform.scale(scale, scale);
transform.translate(-domainStart.getX(), -domainStart.getY());
return transform;
}
/**
* Checks whether the argument is close to 0
*
* @param d to check
*
* @return {@code true} if the argument is close to 0
*/
protected static boolean isZero(double d) {
return Math.abs(d) < ZERO_EPSILON;
}
private AbstractPdfShading constructPdfShading(T[] baseCoordinatesVector,
List<GradientColorStop> stops, GradientSpreadMethod spreadMethod,
Rectangle targetBoundingBox, PdfDocument pdfDocument) {
double baseVectorLength = getBaseVectorLength(baseCoordinatesVector);
List<GradientColorStop> normalizedStops = normalizeStops(stops, baseVectorLength);
List<GradientColorStop> stopsToConstruct;
double[] coordinatesDomain = new double[] {0, 1};
T[] actualCoordinates;
if (isZero(baseVectorLength)
|| normalizedStops.size() == 1) {
// single color case
if (spreadMethod == GradientSpreadMethod.NONE) {
return null;
}
actualCoordinates = createCoveringCoordinates(targetBoundingBox);
GradientColorStop lastColorStop = normalizedStops.get(normalizedStops.size() - 1);
stopsToConstruct = Arrays.asList(new GradientColorStop(lastColorStop, 0d, OffsetType.RELATIVE),
new GradientColorStop(lastColorStop, 1d, OffsetType.RELATIVE));
} else {
coordinatesDomain = computeCoveringDomain(baseCoordinatesVector, targetBoundingBox);
if (spreadMethod == GradientSpreadMethod.REPEAT || spreadMethod == GradientSpreadMethod.REFLECT) {
stopsToConstruct = adjustNormalizedStopsToCoverDomain(
normalizedStops, coordinatesDomain, spreadMethod, pdfDocument);
} else if (spreadMethod == GradientSpreadMethod.PAD) {
stopsToConstruct = adjustStopsForPadIfNeeded(normalizedStops, coordinatesDomain);
} else {
// none case
stopsToConstruct = normalizedStops;
double firstStopOffset = stopsToConstruct.get(0).getOffset();
double lastStopOffset = stopsToConstruct.get(stopsToConstruct.size() - 1).getOffset();
if (isZero(lastStopOffset - firstStopOffset)
|| coordinatesDomain[1] <= firstStopOffset
|| coordinatesDomain[0] >= lastStopOffset) {
return null;
}
coordinatesDomain[0] = Math.max(coordinatesDomain[0], firstStopOffset);
coordinatesDomain[1] = Math.min(coordinatesDomain[1], lastStopOffset);
}
assert coordinatesDomain[0] <= coordinatesDomain[1];
actualCoordinates = createCoordsForNewDomain(coordinatesDomain, baseCoordinatesVector);
}
return createPdfShading(new PdfDeviceCs.Rgb(), createCoordsDictEntry(actualCoordinates),
new PdfArray(coordinatesDomain), constructFunction(stopsToConstruct));
}
private List<GradientColorStop> adjustNormalizedStopsToCoverDomain(List<GradientColorStop> normalizedStops,
double[] targetDomain, GradientSpreadMethod spreadMethod, PdfDocument pdfDocument) {
List<GradientColorStop> adjustedStops = new ArrayList<>();
GradientColorStop lastColorStop = normalizedStops.get(normalizedStops.size() - 1);
double originalIntervalEnd = lastColorStop.getOffset();
double originalIntervalStart = normalizedStops.get(0).getOffset();
double originalIntervalLength = originalIntervalEnd - originalIntervalStart;
if (originalIntervalLength <= ZERO_EPSILON) {
return Arrays.asList(new GradientColorStop(lastColorStop, targetDomain[0], OffsetType.RELATIVE),
new GradientColorStop(lastColorStop, targetDomain[1], OffsetType.RELATIVE));
}
double startIntervalsShift = Math.floor((targetDomain[0] - originalIntervalStart) / originalIntervalLength);
double iterationOffset = originalIntervalStart + (originalIntervalLength * startIntervalsShift);
boolean isIterationInverse =
spreadMethod == GradientSpreadMethod.REFLECT && Math.abs(startIntervalsShift) % 2 != 0;
int currentIterationIndex = isIterationInverse ? normalizedStops.size() - 1 : 0;
int maxStopCount = getMaxStopCountForRepeatAndReflect(pdfDocument);
double lastComputedOffset = iterationOffset;
while (lastComputedOffset <= targetDomain[1]) {
GradientColorStop currentStop = normalizedStops.get(currentIterationIndex);
lastComputedOffset = isIterationInverse ?
iterationOffset + originalIntervalEnd - currentStop.getOffset()
: iterationOffset + currentStop.getOffset() - originalIntervalStart;
GradientColorStop computedStop = new GradientColorStop(currentStop, lastComputedOffset,
OffsetType.RELATIVE);
if (lastComputedOffset < targetDomain[0] && !adjustedStops.isEmpty()) {
adjustedStops.set(0, computedStop);
} else {
adjustedStops.add(computedStop);
}
if (adjustedStops.size() > maxStopCount) {
LoggerFactory.getLogger(getClass()).warn(
MessageFormatUtil.format(KernelLogMessageConstant.GRADIENT_MAX_COLOR_STOPS, maxStopCount));
return adjustStopsForPadIfNeeded(normalizedStops, targetDomain);
}
if (isIterationInverse) {
--currentIterationIndex;
if (currentIterationIndex < 0) {
iterationOffset += originalIntervalLength;
isIterationInverse = false;
currentIterationIndex = 1;
}
} else {
++currentIterationIndex;
if (currentIterationIndex == normalizedStops.size()) {
iterationOffset += originalIntervalLength;
isIterationInverse = spreadMethod == GradientSpreadMethod.REFLECT;
currentIterationIndex = isIterationInverse ? normalizedStops.size() - 2 : 0;
}
}
// check the next iteration type to set the correct stop color hint for just added stop
if (isIterationInverse) {
GradientColorStop nextColor = normalizedStops.get(currentIterationIndex);
// this method should be invoked only after the normalization. it means that
// the hint offset type for each stop is either relative to colors interval
// (i.e. for inverse iteration we need to inverse the hint offset), or is none
// (i.e. the hint offset value should be ignored)
computedStop.setHint(1 - nextColor.getHintOffset(), nextColor.getHintOffsetType());
} else {
computedStop.setHint(currentStop.getHintOffset(), currentStop.getHintOffsetType());
}
}
return adjustedStops;
}
// the result list would have the same list of stop colors as the original one
// with all offsets on coordinates domain dimension and adjusted for ascending values
private static List<GradientColorStop> normalizeStops(List<GradientColorStop> toNormalize,
double baseVectorLength) {
if (isZero(baseVectorLength)) {
return Collections.singletonList(new GradientColorStop(toNormalize.get(toNormalize.size() - 1),
0d, OffsetType.RELATIVE));
}
// get rid of all absolute on vector offsets and hint offsets
List<GradientColorStop> result = copyStopsAndNormalizeAbsoluteOffsets(toNormalize, baseVectorLength);
// normalize 1st stop as it may be a special case
normalizeFirstStopOffset(result);
// now we have 1st stop with relative offset, all other stops are either auto or relative
normalizeAutoStops(result);
// normalize hints to left only none or relative to colors hint offset types
normalizeHintsOffsets(result);
return result;
}
private static void normalizeHintsOffsets(List<GradientColorStop> result) {
// normalize all except last
for (int i = 0; i < result.size() - 1; ++i) {
GradientColorStop stopColor = result.get(i);
if (stopColor.getHintOffsetType() == HintOffsetType.RELATIVE_ON_GRADIENT) {
double currentStopOffset = stopColor.getOffset();
double nextStopOffset = result.get(i + 1).getOffset();
if (currentStopOffset == nextStopOffset) {
// if stops has the same offset, then no hint needed
stopColor.setHint(0, HintOffsetType.NONE);
} else {
double hintOffset = (stopColor.getHintOffset() - currentStopOffset)
/ (nextStopOffset - currentStopOffset);
stopColor.setHint(hintOffset, HintOffsetType.RELATIVE_BETWEEN_COLORS);
}
}
}
// the last color hint is not needed as even with pad and reflect it won't be used
result.get(result.size() - 1).setHint(0, HintOffsetType.NONE);
}
private static void normalizeAutoStops(List<GradientColorStop> toNormalize) {
assert toNormalize.get(0).getOffsetType() == OffsetType.RELATIVE;
int firstAutoStopIndex = 1;
GradientColorStop firstStopColor = toNormalize.get(0);
double prevOffset = firstStopColor.getHintOffsetType() == HintOffsetType.RELATIVE_ON_GRADIENT
? firstStopColor.getHintOffset() : firstStopColor.getOffset();
for (int i = 1; i < toNormalize.size(); ++i) {
GradientColorStop currentStop = toNormalize.get(i);
if (currentStop.getOffsetType() == OffsetType.AUTO) {
if (currentStop.getHintOffsetType() == HintOffsetType.RELATIVE_ON_GRADIENT) {
double hintOffset = currentStop.getHintOffset();
normalizeAutoStops(toNormalize, firstAutoStopIndex, i + 1, prevOffset, hintOffset);
prevOffset = hintOffset;
firstAutoStopIndex = i + 1;
}
} else {
if (firstAutoStopIndex < i) {
// current stop offset is relative
double offset = currentStop.getOffset();
normalizeAutoStops(toNormalize, firstAutoStopIndex, i, prevOffset, offset);
}
firstAutoStopIndex = i + 1;
prevOffset = currentStop.getHintOffsetType() == HintOffsetType.RELATIVE_ON_GRADIENT
? currentStop.getHintOffset() : currentStop.getOffset();
}
}
// check whether the last interval has auto
if (firstAutoStopIndex < toNormalize.size()) {
double lastStopOffset = Math.max(1, prevOffset);
normalizeAutoStops(toNormalize, firstAutoStopIndex, toNormalize.size(), prevOffset, lastStopOffset);
}
}
private static void normalizeAutoStops(List<GradientColorStop> toNormalizeList,
int fromIndex, int toIndex, double prevOffset, double nextOffset) {
assert toIndex >= fromIndex;
int intervalsCount = Math.min(toIndex, toNormalizeList.size() - 1) - fromIndex + 1;
double offsetShift = (nextOffset - prevOffset) / intervalsCount;
double currentOffset = prevOffset;
for (int i = fromIndex; i < toIndex; ++i) {
currentOffset += offsetShift;
GradientColorStop currentAutoStop = toNormalizeList.get(i);
assert currentAutoStop.getOffsetType() == OffsetType.AUTO;
currentAutoStop.setOffset(currentOffset, OffsetType.RELATIVE);
}
}
private static void normalizeFirstStopOffset(List<GradientColorStop> result) {
// assert that all stops has no absolute on vector offsets and hints
GradientColorStop firstStop = result.get(0);
if (firstStop.getOffsetType() != OffsetType.AUTO) {
return;
}
double firstStopOffset = 0;
for (GradientColorStop stopColor : result) {
if (stopColor.getOffsetType() == OffsetType.RELATIVE) {
firstStopOffset = stopColor.getOffset();
break;
} else if (stopColor.getHintOffsetType() == HintOffsetType.RELATIVE_ON_GRADIENT) {
firstStopOffset = stopColor.getHintOffset();
break;
}
}
firstStopOffset = Math.min(0, firstStopOffset);
firstStop.setOffset(firstStopOffset, OffsetType.RELATIVE);
}
private static List<GradientColorStop> copyStopsAndNormalizeAbsoluteOffsets(List<GradientColorStop> toNormalize,
double baseVectorLength) {
double lastUsedOffset = Double.NEGATIVE_INFINITY;
List<GradientColorStop> copy = new ArrayList<>(toNormalize.size());
for (GradientColorStop stop : toNormalize) {
double offset = stop.getOffset();
OffsetType offsetType = stop.getOffsetType();
if (offsetType == OffsetType.ABSOLUTE) {
offsetType = OffsetType.RELATIVE;
offset /= baseVectorLength;
}
if (offsetType == OffsetType.RELATIVE) {
if (offset < lastUsedOffset) {
offset = lastUsedOffset;
}
lastUsedOffset = offset;
}
GradientColorStop result = new GradientColorStop(stop, offset, offsetType);
double hintOffset = stop.getHintOffset();
HintOffsetType hintOffsetType = stop.getHintOffsetType();
if (hintOffsetType == HintOffsetType.ABSOLUTE_ON_GRADIENT) {
hintOffsetType = HintOffsetType.RELATIVE_ON_GRADIENT;
hintOffset /= baseVectorLength;
}
if (hintOffsetType == HintOffsetType.RELATIVE_ON_GRADIENT) {
if (hintOffset < lastUsedOffset) {
hintOffset = lastUsedOffset;
}
lastUsedOffset = hintOffset;
}
result.setHint(hintOffset, hintOffsetType);
copy.add(result);
}
return copy;
}
private static List<GradientColorStop> adjustStopsForPadIfNeeded(List<GradientColorStop> normalizedStops,
double[] coordinatesDomain) {
List<GradientColorStop> stopsToConstruct = new ArrayList<>(normalizedStops.size() + 2);
GradientColorStop firstStop = normalizedStops.get(0);
if (coordinatesDomain[0] < firstStop.getOffset()) {
stopsToConstruct.add(0, new GradientColorStop(firstStop, coordinatesDomain[0], OffsetType.RELATIVE));
}
stopsToConstruct.addAll(normalizedStops);
GradientColorStop lastStop = normalizedStops.get(normalizedStops.size() - 1);
if (coordinatesDomain[1] > lastStop.getOffset()) {
stopsToConstruct.add(new GradientColorStop(lastStop, coordinatesDomain[1], OffsetType.RELATIVE));
}
return stopsToConstruct;
}
private static IPdfFunction constructFunction(List<GradientColorStop> toConstruct) {
int functionsAmount = toConstruct.size() - 1;
double[] bounds = new double[functionsAmount - 1];
List<AbstractPdfFunction<? extends PdfDictionary>> type2Functions = new ArrayList<>(functionsAmount);
GradientColorStop currentStop;
GradientColorStop nextStop = toConstruct.get(0);
double domainStart = nextStop.getOffset();
for (int i = 1; i < functionsAmount; ++i) {
currentStop = nextStop;
nextStop = toConstruct.get(i);
bounds[i - 1] = nextStop.getOffset();
type2Functions.add(constructSingleGradientSegmentFunction(currentStop, nextStop));
}
currentStop = nextStop;
nextStop = toConstruct.get(toConstruct.size() - 1);
type2Functions.add(constructSingleGradientSegmentFunction(currentStop, nextStop));
double domainEnd = nextStop.getOffset();
double[] encode = new double[functionsAmount * 2];
for (int i = 0; i < encode.length; i += 2) {
encode[i] = 0d;
encode[i + 1] = 1d;
}
return new PdfType3Function(new double[] {domainStart, domainEnd}, null, type2Functions, bounds, encode);
}
private static AbstractPdfFunction<? extends PdfDictionary> constructSingleGradientSegmentFunction(
GradientColorStop from, GradientColorStop to) {
double exponent = 1d;
float[] fromColor = from.getRgbArray();
float[] toColor = to.getRgbArray();
if (from.getHintOffsetType() == HintOffsetType.RELATIVE_BETWEEN_COLORS) {
double hintOffset = from.getHintOffset();
if (hintOffset <= 0d + ZERO_EPSILON) {
fromColor = toColor;
} else if (hintOffset >= 1d - ZERO_EPSILON) {
toColor = fromColor;
} else {
// similar to css color hint logic
exponent = Math.log(0.5) / Math.log(hintOffset);
}
}
return new PdfType2Function(new float[] {0f, 1f}, null, fromColor, toColor, exponent);
}
private static int getMaxStopCountForRepeatAndReflect(PdfDocument pdfDocument) {
int result = GradientPropertiesResolver.DEFAULT_MAX_COLOR_STOPS;
if (pdfDocument != null) {
GradientPropertiesResolver gradientPropertiesResolver = pdfDocument.getDiContainer()
.getInstance(GradientPropertiesResolver.class);
if (gradientPropertiesResolver != null) {
result = gradientPropertiesResolver.getMaxColorStops();
}
}
return result;
}
}