Skip to content

Commit afc4804

Browse files
committed
Linear Gradient update to handle pad properly, Css gradient parsing fix, tests updates
DEVSIX-2086
1 parent 2ea552f commit afc4804

File tree

69 files changed

+162
-85
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+162
-85
lines changed

kernel/src/main/java/com/itextpdf/kernel/colors/gradients/AbstractLinearGradientBuilder.java

Lines changed: 60 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,17 @@ public GradientSpreadMethod getSpreadMethod() {
123123
* the current space. The {@code null} value is valid and can be used
124124
* if there is no transformation from base coordinates to current space
125125
* specified, or it is equal to identity transformation.
126-
* @return the constructed {@link Color}
126+
* @return the constructed {@link Color} or {@code null} if no color to be applied
127+
* or base gradient vector has been specified
127128
*/
128129
public Color buildColor(Rectangle targetBoundingBox, AffineTransform contextTransform) {
129-
Point[] coordinates = getGradientVector(targetBoundingBox, contextTransform);
130-
if (coordinates == null || this.stops.isEmpty()) {
131-
// Can not create gradient color with 0 stops
130+
Point[] baseCoordinatesVector = getGradientVector(targetBoundingBox, contextTransform);
131+
if (baseCoordinatesVector == null || this.stops.isEmpty()) {
132+
// Can not create gradient color with 0 stops or null coordinates vector
132133
return null;
133-
} else if (this.stops.size() == 1 || coordinates[0].equals(coordinates[1])) {
134-
// single stop and zero vector case
135-
float[] lastStopRgb = this.stops.get(this.stops.size() - 1).getRgbArray();
136-
return new DeviceRgb(lastStopRgb[0], lastStopRgb[1], lastStopRgb[2]);
137134
}
138135

139136
// evaluate actual coordinates and transformation
140-
Point[] baseCoordinatesVector = new Point[] {coordinates[0].getLocation(), coordinates[1].getLocation()};
141137
AffineTransform shadingTransform = new AffineTransform();
142138
if (contextTransform != null) {
143139
shadingTransform.concatenate(contextTransform);
@@ -167,6 +163,9 @@ public Color buildColor(Rectangle targetBoundingBox, AffineTransform contextTran
167163

168164
PdfShading.Axial axial = createAxialShading(baseCoordinatesVector, this.stops, this.spreadMethod,
169165
targetBoundingBox);
166+
if (axial == null) {
167+
return null;
168+
}
170169

171170
PdfPattern.Shading shading = new PdfPattern.Shading(axial);
172171
if (!shadingTransform.isIdentity()) {
@@ -271,91 +270,61 @@ protected static Point[] createCoordinatesForNewDomain(double[] newDomain, Point
271270

272271
private static PdfShading.Axial createAxialShading(Point[] baseCoordinatesVector,
273272
List<GradientColorStop> stops, GradientSpreadMethod spreadMethod, Rectangle targetBoundingBox) {
274-
List<GradientColorStop> stopsToConstruct = normalizeStops(stops, baseCoordinatesVector);
273+
double baseVectorLength = baseCoordinatesVector[1].distance(baseCoordinatesVector[0]);
275274

275+
List<GradientColorStop> stopsToConstruct = normalizeStops(stops, baseVectorLength);
276276
double[] coordinatesDomain = new double[] {0, 1};
277-
if (spreadMethod == GradientSpreadMethod.REPEAT || spreadMethod == GradientSpreadMethod.REFLECT) {
278-
coordinatesDomain = evaluateCoveringDomain(baseCoordinatesVector, targetBoundingBox);
279-
stopsToConstruct = adjustNormalizedStopsToCoverDomain(stopsToConstruct, coordinatesDomain, spreadMethod);
277+
Point[] actualCoordinates;
278+
if (baseVectorLength < ZERO_EPSILON || stopsToConstruct.size() == 1) {
279+
// single color case
280+
if (spreadMethod == GradientSpreadMethod.NONE) {
281+
return null;
282+
}
283+
actualCoordinates = new Point[]{new Point(targetBoundingBox.getLeft(), targetBoundingBox.getBottom()),
284+
new Point(targetBoundingBox.getRight(), targetBoundingBox.getBottom())};
285+
286+
GradientColorStop lastColorStop = stopsToConstruct.get(stopsToConstruct.size() - 1);
287+
stopsToConstruct = Arrays.asList(new GradientColorStop(lastColorStop, 0d, OffsetType.RELATIVE),
288+
new GradientColorStop(lastColorStop, 1d, OffsetType.RELATIVE));
280289
} else {
281-
// to ensure that stops list covers the full domain. For repeat and reflect cases
282-
// this is done by adjusting the initial stops to cover the evaluated domain
283-
coordinatesDomain[0] = Math.max(coordinatesDomain[0], stopsToConstruct.get(0).getOffset());
284-
coordinatesDomain[1] = Math.min(coordinatesDomain[1],
285-
stopsToConstruct.get(stopsToConstruct.size() - 1).getOffset());
286-
coordinatesDomain[1] = Math.max(coordinatesDomain[0], coordinatesDomain[1]);
287-
}
290+
coordinatesDomain = evaluateCoveringDomain(baseCoordinatesVector, targetBoundingBox);
291+
if (spreadMethod == GradientSpreadMethod.REPEAT || spreadMethod == GradientSpreadMethod.REFLECT) {
292+
stopsToConstruct = adjustNormalizedStopsToCoverDomain(stopsToConstruct, coordinatesDomain,
293+
spreadMethod);
294+
} else if (spreadMethod == GradientSpreadMethod.PAD) {
295+
adjustStopsForPadIfNeeded(stopsToConstruct, coordinatesDomain);
296+
} else {
297+
// none case
298+
double firstStopOffset = stopsToConstruct.get(0).getOffset();
299+
double lastStopOffset = stopsToConstruct.get(stopsToConstruct.size() - 1).getOffset();
300+
if ((lastStopOffset - firstStopOffset < ZERO_EPSILON)
301+
|| coordinatesDomain[1] <= firstStopOffset
302+
|| coordinatesDomain[0] >= lastStopOffset) {
303+
return null;
304+
}
305+
coordinatesDomain[0] = Math.max(coordinatesDomain[0], firstStopOffset);
306+
coordinatesDomain[1] = Math.min(coordinatesDomain[1], lastStopOffset);
307+
}
308+
assert coordinatesDomain[0] <= coordinatesDomain[1];
288309

289-
// workaround for PAD case
290-
if (spreadMethod == GradientSpreadMethod.PAD) {
291-
coordinatesDomain = modifyNormalizedStopsForPad(stopsToConstruct, coordinatesDomain);
310+
actualCoordinates = createCoordinatesForNewDomain(coordinatesDomain, baseCoordinatesVector);
292311
}
293312

294-
assert coordinatesDomain[0] <= coordinatesDomain[1];
295-
296-
Point[] actualCoordinates = createCoordinatesForNewDomain(coordinatesDomain, baseCoordinatesVector);
297-
298-
PdfShading.Axial axial = new PdfShading.Axial(
313+
return new PdfShading.Axial(
299314
new PdfDeviceCs.Rgb(),
300315
createCoordsPdfArray(actualCoordinates),
301316
new PdfArray(coordinatesDomain),
302317
constructFunction(stopsToConstruct)
303318
);
304-
// apply extended flag for PAD case
305-
if (spreadMethod == GradientSpreadMethod.PAD) {
306-
axial.setExtend(true, true);
307-
}
308-
return axial;
309-
}
310-
311-
private static double[] modifyNormalizedStopsForPad(List<GradientColorStop> stopsToConstruct,
312-
double[] coordinatesDomain) {
313-
double[] newDomain = new double[] {coordinatesDomain[0], coordinatesDomain[1]};
314-
double eps = Math.max(1, (coordinatesDomain[1] - coordinatesDomain[0])) * 0.05;
315-
316-
for (int i = stopsToConstruct.size() - 1; i > 0; --i) {
317-
GradientColorStop currentStop = stopsToConstruct.get(i);
318-
double currentStopOffset = currentStop.getOffset();
319-
if (Math.abs(currentStopOffset - coordinatesDomain[1]) < eps) {
320-
GradientColorStop prevStop = stopsToConstruct.get(i - 1);
321-
if ((prevStop.getHintOffsetType() == HintOffsetType.RELATIVE_BETWEEN_COLORS
322-
&& prevStop.getHintOffset() >= 1d - ZERO_EPSILON)
323-
|| (prevStop.getOffset() > currentStopOffset - eps)) {
324-
double lastOffset = currentStopOffset + eps;
325-
stopsToConstruct.add(i + 1, new GradientColorStop(currentStop, lastOffset, OffsetType.RELATIVE));
326-
newDomain[1] = lastOffset;
327-
}
328-
break;
329-
}
330-
if (currentStopOffset < coordinatesDomain[1]) {
331-
break;
332-
}
333-
}
334-
335-
for (int i = 0; i < stopsToConstruct.size() - 1; ++i) {
336-
GradientColorStop currentStop = stopsToConstruct.get(i);
337-
double currentStopOffset = currentStop.getOffset();
338-
if (Math.abs(currentStopOffset - coordinatesDomain[0]) < eps) {
339-
if ((currentStop.getHintOffsetType() == HintOffsetType.RELATIVE_BETWEEN_COLORS
340-
&& currentStop.getHintOffset() <= 0d + ZERO_EPSILON)
341-
|| (stopsToConstruct.get(i + 1).getOffset() < currentStopOffset + eps)) {
342-
double firstOffset = currentStopOffset - eps;
343-
stopsToConstruct.add(i, new GradientColorStop(currentStop, firstOffset, OffsetType.RELATIVE));
344-
newDomain[0] = firstOffset;
345-
}
346-
break;
347-
}
348-
if (currentStopOffset > coordinatesDomain[0]) {
349-
break;
350-
}
351-
}
352-
return newDomain;
353319
}
354320

355321
// the result list would have the same list of stop colors as the original one
356322
// with all offsets on coordinates domain dimension and adjusted for ascending values
357-
private static List<GradientColorStop> normalizeStops(List<GradientColorStop> toNormalize, Point[] coordinates) {
358-
double baseVectorLength = coordinates[1].distance(coordinates[0]);
323+
private static List<GradientColorStop> normalizeStops(List<GradientColorStop> toNormalize, double baseVectorLength) {
324+
if (baseVectorLength < ZERO_EPSILON) {
325+
return Arrays.asList(new GradientColorStop(toNormalize.get(toNormalize.size() - 1),
326+
0d, OffsetType.RELATIVE));
327+
}
359328
// get rid of all absolute on vector offsets and hint offsets
360329
List<GradientColorStop> result = copyStopsAndNormalizeAbsoluteOffsets(toNormalize, baseVectorLength);
361330
// normalize 1st stop as it may be a special case
@@ -502,6 +471,18 @@ private static List<GradientColorStop> copyStopsAndNormalizeAbsoluteOffsets(List
502471
return copy;
503472
}
504473

474+
private static void adjustStopsForPadIfNeeded(List<GradientColorStop> stopsToConstruct,
475+
double[] coordinatesDomain) {
476+
GradientColorStop firstStop = stopsToConstruct.get(0);
477+
if (coordinatesDomain[0] < firstStop.getOffset()) {
478+
stopsToConstruct.add(0, new GradientColorStop(firstStop, coordinatesDomain[0], OffsetType.RELATIVE));
479+
}
480+
GradientColorStop lastStop = stopsToConstruct.get(stopsToConstruct.size() - 1);
481+
if (coordinatesDomain[1] > lastStop.getOffset()) {
482+
stopsToConstruct.add(new GradientColorStop(lastStop, coordinatesDomain[1], OffsetType.RELATIVE));
483+
}
484+
}
485+
505486
private static List<GradientColorStop> adjustNormalizedStopsToCoverDomain(List<GradientColorStop> normalizedStops,
506487
double[] targetDomain, GradientSpreadMethod spreadMethod) {
507488
List<GradientColorStop> adjustedStops = new ArrayList<>();

kernel/src/main/java/com/itextpdf/kernel/colors/gradients/LinearGradientBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public LinearGradientBuilder setCurrentSpaceToGradientVectorSpaceTransformation(
7474

7575
@Override
7676
public Point[] getGradientVector(Rectangle targetBoundingBox, AffineTransform contextTransform) {
77-
return this.coordinates;
77+
return new Point[] {this.coordinates[0].getLocation(), this.coordinates[1].getLocation()};
7878
}
7979

8080
@Override

kernel/src/test/java/com/itextpdf/kernel/colors/gradients/LinearGradientBuilderTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,71 @@ public void builderWithZeroColorsLengthAndReflect() throws IOException, Interrup
695695
generateAndComparePdfs("builderWithZeroColorsLengthAndReflect.pdf", targetBoundingBox, null, gradientBuilder);
696696
}
697697

698+
@Test
699+
public void buildWithTwoStopsBeforeTheBeginningAndNoneTest() throws IOException, InterruptedException {
700+
Rectangle targetBoundingBox = new Rectangle(50f, 450f, 300f, 300f);
701+
AbstractLinearGradientBuilder gradientBuilder = new LinearGradientBuilder()
702+
.setGradientVector(targetBoundingBox.getLeft() + 100f, targetBoundingBox.getBottom() + 100f,
703+
targetBoundingBox.getRight() - 100f, targetBoundingBox.getTop() - 100f)
704+
.setSpreadMethod(GradientSpreadMethod.NONE)
705+
.addColorStop(new GradientColorStop(ColorConstants.RED.getColorValue(), -10d, OffsetType.RELATIVE))
706+
.addColorStop(new GradientColorStop(ColorConstants.BLUE.getColorValue(), -5d, OffsetType.RELATIVE));
707+
708+
Assert.assertNull(gradientBuilder.buildColor(targetBoundingBox, null));
709+
}
710+
711+
@Test
712+
public void buildWithTwoStopsAfterEndAndNoneTest() throws IOException, InterruptedException {
713+
Rectangle targetBoundingBox = new Rectangle(50f, 450f, 300f, 300f);
714+
AbstractLinearGradientBuilder gradientBuilder = new LinearGradientBuilder()
715+
.setGradientVector(targetBoundingBox.getLeft() + 100f, targetBoundingBox.getBottom() + 100f,
716+
targetBoundingBox.getRight() - 100f, targetBoundingBox.getTop() - 100f)
717+
.setSpreadMethod(GradientSpreadMethod.NONE)
718+
.addColorStop(new GradientColorStop(ColorConstants.RED.getColorValue(), 5d, OffsetType.RELATIVE))
719+
.addColorStop(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 10d, OffsetType.RELATIVE));
720+
721+
Assert.assertNull(gradientBuilder.buildColor(targetBoundingBox, null));
722+
}
723+
724+
@Test
725+
public void buildWithTwoEqualOffsetsStopsAndNoneTest() throws IOException, InterruptedException {
726+
Rectangle targetBoundingBox = new Rectangle(50f, 450f, 300f, 300f);
727+
AbstractLinearGradientBuilder gradientBuilder = new LinearGradientBuilder()
728+
.setGradientVector(targetBoundingBox.getLeft() + 100f, targetBoundingBox.getBottom() + 100f,
729+
targetBoundingBox.getRight() - 100f, targetBoundingBox.getTop() - 100f)
730+
.setSpreadMethod(GradientSpreadMethod.NONE)
731+
.addColorStop(new GradientColorStop(ColorConstants.RED.getColorValue(), 0.5d, OffsetType.RELATIVE))
732+
.addColorStop(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 0.5d, OffsetType.RELATIVE));
733+
734+
Assert.assertNull(gradientBuilder.buildColor(targetBoundingBox, null));
735+
}
736+
737+
@Test
738+
public void buildWithTwoStopsInCenterAndNoneTest() throws IOException, InterruptedException {
739+
Rectangle targetBoundingBox = new Rectangle(50f, 450f, 300f, 300f);
740+
AbstractLinearGradientBuilder gradientBuilder = new LinearGradientBuilder()
741+
.setGradientVector(targetBoundingBox.getLeft() + 100f, targetBoundingBox.getBottom() + 100f,
742+
targetBoundingBox.getRight() - 100f, targetBoundingBox.getTop() - 100f)
743+
.setSpreadMethod(GradientSpreadMethod.NONE)
744+
.addColorStop(new GradientColorStop(ColorConstants.RED.getColorValue(), 0.2d, OffsetType.RELATIVE))
745+
.addColorStop(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 0.8d, OffsetType.RELATIVE));
746+
747+
generateAndComparePdfs("buildWithTwoEqualOffsetsStopsTest.pdf", targetBoundingBox, null, gradientBuilder);
748+
}
749+
750+
@Test
751+
public void buildWithTwoStopsOutsideAndNoneTest() throws IOException, InterruptedException {
752+
Rectangle targetBoundingBox = new Rectangle(50f, 450f, 300f, 300f);
753+
AbstractLinearGradientBuilder gradientBuilder = new LinearGradientBuilder()
754+
.setGradientVector(targetBoundingBox.getLeft() + 100f, targetBoundingBox.getBottom() + 100f,
755+
targetBoundingBox.getRight() - 100f, targetBoundingBox.getTop() - 100f)
756+
.setSpreadMethod(GradientSpreadMethod.NONE)
757+
.addColorStop(new GradientColorStop(ColorConstants.RED.getColorValue(), -1.5d, OffsetType.RELATIVE))
758+
.addColorStop(new GradientColorStop(ColorConstants.BLUE.getColorValue(), 2.5d, OffsetType.RELATIVE));
759+
760+
generateAndComparePdfs("buildWithTwoStopsOutsideAndNoneTest.pdf", targetBoundingBox, null, gradientBuilder);
761+
}
762+
698763
private void generateAndComparePdfs(String fileName, Rectangle toDraw, AffineTransform transform,
699764
AbstractLinearGradientBuilder gradientBuilder) throws InterruptedException, IOException {
700765
String outPdfPath = destinationFolder + fileName;

0 commit comments

Comments
 (0)