diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/AbstractPropertyBuilder.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/AbstractPropertyBuilder.java
index bf0dfda02..108b42f35 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/AbstractPropertyBuilder.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/AbstractPropertyBuilder.java
@@ -164,6 +164,10 @@ protected void checkIdentLengthNumberOrPercentType(CSSName cssName, CSSPrimitive
}
protected boolean isLength(CSSPrimitiveValue value) {
+ return isLengthHelper(value);
+ }
+
+ public static boolean isLengthHelper(CSSPrimitiveValue value) {
int unit = value.getPrimitiveType();
return unit == CSSPrimitiveValue.CSS_EMS || unit == CSSPrimitiveValue.CSS_EXS
|| unit == CSSPrimitiveValue.CSS_PX || unit == CSSPrimitiveValue.CSS_IN
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java
index ed4b853b8..b636aed3e 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/parser/property/PrimitivePropertyBuilders.java
@@ -529,6 +529,24 @@ public static class BackgroundColor extends GenericColor {
}
public static class BackgroundImage extends GenericURIWithNone {
+ @Override
+ public List buildDeclarations(
+ CSSName cssName, List values, int origin,
+ boolean important, boolean inheritAllowed) {
+
+ checkValueCount(cssName, 1, values.size());
+ PropertyValue value = values.get(0);
+
+ if (value.getPropertyValueType() == PropertyValue.VALUE_TYPE_FUNCTION &&
+ Objects.equals(value.getFunction().getName(), "linear-gradient")) {
+ // TODO: Validation of linear-gradient args.
+ return Collections.singletonList(
+ new PropertyDeclaration(cssName, value, important, origin));
+ } else {
+ return super.buildDeclarations(cssName, values, origin, important, inheritAllowed);
+ }
+ }
+
}
public static class BackgroundSize extends AbstractPropertyBuilder {
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java
index 666bfd826..8ed2288d3 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/CalculatedStyle.java
@@ -22,6 +22,7 @@
import java.awt.Cursor;
import java.util.List;
+import java.util.Objects;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -39,6 +40,7 @@
import com.openhtmltopdf.css.style.derived.BorderPropertySet;
import com.openhtmltopdf.css.style.derived.CountersValue;
import com.openhtmltopdf.css.style.derived.DerivedValueFactory;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.css.style.derived.FunctionValue;
import com.openhtmltopdf.css.style.derived.LengthValue;
import com.openhtmltopdf.css.style.derived.ListValue;
@@ -1393,9 +1395,22 @@ public static int getCSSMaxHeight(CssContext c, Box box) {
return (int) cssMaxHeight.value();
}
}
-
-
-}// end class
+
+ public boolean isLinearGradient() {
+ FSDerivedValue value = valueByName(CSSName.BACKGROUND_IMAGE);
+ return value instanceof FunctionValue &&
+ Objects.equals(((FunctionValue) value).getFunction().getName(), "linear-gradient");
+ }
+
+ public FSLinearGradient getLinearGradient(CssContext cssContext, int boxWidth, int boxHeight) {
+ if (!isLinearGradient()) {
+ return null;
+ }
+
+ FunctionValue value = (FunctionValue) valueByName(CSSName.BACKGROUND_IMAGE);
+ return new FSLinearGradient(this, value.getFunction(), boxWidth, boxHeight, cssContext);
+ }
+}
/*
* $Id$
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java
new file mode 100644
index 000000000..d386ab6de
--- /dev/null
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java
@@ -0,0 +1,370 @@
+package com.openhtmltopdf.css.style.derived;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import com.openhtmltopdf.css.constants.CSSName;
+import com.openhtmltopdf.css.constants.Idents;
+import com.openhtmltopdf.css.parser.CSSPrimitiveValue;
+import com.openhtmltopdf.css.parser.FSColor;
+import com.openhtmltopdf.css.parser.FSFunction;
+import com.openhtmltopdf.css.parser.PropertyValue;
+import com.openhtmltopdf.css.parser.property.AbstractPropertyBuilder;
+import com.openhtmltopdf.css.parser.property.Conversions;
+import com.openhtmltopdf.css.style.CalculatedStyle;
+import com.openhtmltopdf.css.style.CssContext;
+
+public class FSLinearGradient {
+
+ /**
+ * A stop point which does not yet have a length.
+ * We need all the stop points first before we can calculate
+ * a length for intermediate stop points without a length.
+ */
+ private static class IntermediateStopPoint {
+ private final FSColor _color;
+
+ IntermediateStopPoint(FSColor color) {
+ _color = color;
+ }
+
+ public FSColor getColor() {
+ return _color;
+ }
+ }
+
+ public static class StopPoint extends IntermediateStopPoint {
+ private final float _length;
+
+ StopPoint(FSColor color, float length) {
+ super(color);
+ this._length = length;
+ }
+
+ public float getLength() {
+ return _length;
+ }
+
+ @Override
+ public String toString() {
+ return "StopPoint [length=" + _length +
+ ", color=" + getColor() + "]";
+ }
+ }
+
+ private final List _stopPoints;
+ private final float _angle;
+ private int x1;
+ private int x2;
+ private int y1;
+ private int y2;
+
+ public FSLinearGradient(CalculatedStyle style, FSFunction function, int boxWidth, int boxHeight, CssContext ctx) {
+ List params = function.getParameters();
+ int stopsStartIndex = getStopsStartIndex(params);
+
+ float prelimAngle = calculateAngle(params, stopsStartIndex);
+ prelimAngle = prelimAngle % 360f;
+ if (prelimAngle < 0) {
+ prelimAngle += 360f;
+ }
+
+ this._angle = prelimAngle;
+ this._stopPoints = calculateStopPoints(params, style, ctx, boxWidth, stopsStartIndex);
+ endPointsFromAngle(_angle, boxWidth, boxHeight);
+ }
+
+ private float deg2rad(final float deg) {
+ return (float) Math.toRadians(deg);
+ }
+
+ // Compute the endpoints so that a gradient of the given angle
+ // covers a box of the given size.
+ // From: https://github.com/WebKit/webkit/blob/master/Source/WebCore/css/CSSGradientValue.cpp
+ private void endPointsFromAngle(float angleDeg, final int w, final int h) {
+ if (angleDeg == 0) {
+ x1 = 0;
+ y1 = h;
+
+ x2 = 0;
+ y2 = 0;
+ return;
+ }
+
+ if (angleDeg == 90) {
+ x1 = 0;
+ y1 = 0;
+
+ x2 = w;
+ y2 = 0;
+ return;
+ }
+
+ if (angleDeg == 180) {
+ x1 = 0;
+ y1 = 0;
+
+ x2 = 0;
+ y2 = h;
+ return;
+ }
+
+ if (angleDeg == 270) {
+ x1 = w;
+ y1 = 0;
+
+ x2 = 0;
+ y2 = 0;
+ return;
+ }
+
+ // angleDeg is a "bearing angle" (0deg = N, 90deg = E),
+ // but tan expects 0deg = E, 90deg = N.
+ final float slope = (float) Math.tan(deg2rad(90 - angleDeg));
+
+ // We find the endpoint by computing the intersection of the line formed by the
+ // slope,
+ // and a line perpendicular to it that intersects the corner.
+ final float perpendicularSlope = -1 / slope;
+
+ // Compute start corner relative to center, in Cartesian space (+y = up).
+ final float halfHeight = h / 2;
+ final float halfWidth = w / 2;
+ float xEnd, yEnd;
+
+ if (angleDeg < 90) {
+ xEnd = halfWidth;
+ yEnd = halfHeight;
+ } else if (angleDeg < 180) {
+ xEnd = halfWidth;
+ yEnd = -halfHeight;
+ } else if (angleDeg < 270) {
+ xEnd = -halfWidth;
+ yEnd = -halfHeight;
+ } else {
+ xEnd = -halfWidth;
+ yEnd = halfHeight;
+ }
+
+ // Compute c (of y = mx + c) using the corner point.
+ final float c = yEnd - perpendicularSlope * xEnd;
+ final float endX = c / (slope - perpendicularSlope);
+ final float endY = perpendicularSlope * endX + c;
+
+ // We computed the end point, so set the second point,
+ // taking into account the moved origin and the fact that we're in drawing space
+ // (+y = down).
+ x2 = (int) (halfWidth + endX);
+ y2 = (int) (halfHeight - endY);
+
+ // Reflect around the center for the start point.
+ x1 = (int) (halfWidth - endX);
+ y1 = (int) (halfHeight + endY);
+ }
+
+ private boolean isLengthOrPercentage(PropertyValue value) {
+ return AbstractPropertyBuilder.isLengthHelper(value) ||
+ value.getPrimitiveType() == CSSPrimitiveValue.CSS_PERCENTAGE;
+ }
+
+ private List calculateStopPoints(
+ List params, CalculatedStyle style, CssContext ctx, float boxWidth, int stopsStartIndex) {
+
+ List points = new ArrayList<>();
+
+ for (int i = stopsStartIndex; i < params.size();) {
+ PropertyValue value = params.get(i);
+ FSColor color;
+
+ if (value.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) {
+ color = Conversions.getColor(value.getStringValue());
+ } else {
+ color = value.getFSColor();
+ }
+
+ if (i + 1 < params.size() && isLengthOrPercentage(params.get(i + 1))) {
+
+ PropertyValue lengthValue = params.get(i + 1);
+ float length = LengthValue.calcFloatProportionalValue(style, CSSName.BACKGROUND_IMAGE, "",
+ lengthValue.getFloatValue(), lengthValue.getPrimitiveType(), boxWidth, ctx);
+ points.add(new StopPoint(color, length));
+ i += 2;
+ } else {
+ points.add(new IntermediateStopPoint(color));
+ i += 1;
+ }
+ }
+
+ List ret = new ArrayList<>(points.size());
+
+ for (int i = 0; i < points.size(); i++) {
+ IntermediateStopPoint pt = points.get(i);
+ boolean intermediate = pt.getClass() == IntermediateStopPoint.class;
+
+ if (!intermediate) {
+ ret.add((StopPoint) pt);
+ } else if (i == 0) {
+ ret.add(new StopPoint(pt.getColor(), 0f));
+ } else if (i == points.size() - 1) {
+ float len = get100PercentDefaultStopLength(style, ctx, boxWidth);
+ ret.add(new StopPoint(pt.getColor(), len));
+ } else {
+ // Poo, we've got a length-less stop in the middle.
+ // Lets say we have linear-gradient(to right, red, blue 10px, orange, yellow, black 100px, purple):
+ // In this case because orange and yellow don't have lengths we have to devide the difference
+ // between them. So difference = 90px and there are 3 color changes means that the interval
+ // will be 30px and that orange will be at 40px and yellow at 70px.
+ int nextWithLengthIndex = getNextStopPointWithLengthIndex(points, i + 1);
+ int prevWithLengthIndex = getPrevStopPointWithLengthIndex(points, i - 1);
+
+ float nextLength = nextWithLengthIndex == -1 ?
+ get100PercentDefaultStopLength(style, ctx, boxWidth) :
+ ((StopPoint) points.get(nextWithLengthIndex)).getLength();
+
+ float prevLength = prevWithLengthIndex == -1 ? 0 :
+ ((StopPoint) points.get(prevWithLengthIndex)).getLength();
+
+ float range = nextLength - prevLength;
+
+ int topRangeIndex = nextWithLengthIndex == -1 ? points.size() - 1 : nextWithLengthIndex;
+ int bottomRangeIndex = prevWithLengthIndex == -1 ? 0 : prevWithLengthIndex;
+
+ int rangeCount = (topRangeIndex - bottomRangeIndex) + 1;
+ int thisCount = i - bottomRangeIndex;
+
+ // rangeCount should never be zero.
+ if (rangeCount != 0) {
+ float interval = range / rangeCount;
+ float thisLength = prevLength + (interval * thisCount);
+ ret.add(new StopPoint(pt.getColor(), thisLength));
+ }
+ }
+ }
+
+ return ret;
+ }
+
+ private int getPrevStopPointWithLengthIndex(List points, int maxIndex) {
+ for (int i = maxIndex; i >= 0; i--) {
+ if (isStopPointWithLength(points.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private float get100PercentDefaultStopLength(CalculatedStyle style, CssContext ctx, float boxWidth) {
+ return LengthValue.calcFloatProportionalValue(style, CSSName.BACKGROUND_IMAGE, "100%",
+ 100f, CSSPrimitiveValue.CSS_PERCENTAGE, boxWidth, ctx);
+ }
+
+ private boolean isStopPointWithLength(IntermediateStopPoint pt) {
+ return pt.getClass() == StopPoint.class;
+ }
+
+ private int getNextStopPointWithLengthIndex(List points, int startIndex) {
+ for (int i = startIndex; i < points.size(); i++) {
+ if (isStopPointWithLength(points.get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int getStopsStartIndex(List params) {
+ if (Objects.equals(params.get(0).getStringValue(), "to")) {
+ int i = 1;
+ while (i < params.size() &&
+ params.get(i).getStringValue() != null &&
+ Idents.looksLikeABGPosition(params.get(i).getStringValue())) {
+ i++;
+ }
+
+ return i;
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Calculates the angle of the linear gradient in degrees.
+ */
+ private float calculateAngle(List params, int stopsStartIndex) {
+ if (Objects.equals(params.get(0).getStringValue(), "to")) {
+ // The to keyword is followed by one or two position
+ // idents (in any order).
+ // linear-gradient( to left top, blue, red);
+ // linear-gradient( to top right, blue, red);
+ List positions = new ArrayList<>(2);
+
+ for (int i = 1; i < stopsStartIndex; i++) {
+ positions.add(params.get(i).getStringValue());
+ }
+
+ if (positions.contains("top") && positions.contains("left"))
+ return 315f;
+ else if (positions.contains("top") && positions.contains("right"))
+ return 45f;
+ else if (positions.contains("bottom") && positions.contains("left"))
+ return 225f;
+ else if (positions.contains("bottom") && positions.contains("right"))
+ return 135f;
+ else if (positions.contains("bottom"))
+ return 180f;
+ else if (positions.contains("left"))
+ return 270f;
+ else if (positions.contains("right"))
+ return 90f;
+ else
+ return 0f;
+ }
+ else if (params.get(0).getPrimitiveType() == CSSPrimitiveValue.CSS_DEG)
+ {
+ // linear-gradient(45deg, ...)
+ return params.get(0).getFloatValue();
+ }
+ else if (params.get(0).getPrimitiveType() == CSSPrimitiveValue.CSS_RAD)
+ {
+ // linear-gradient(2rad)
+ return params.get(0).getFloatValue() * (float) (180 / Math.PI);
+ }
+ else
+ {
+ return 0f;
+ }
+ }
+
+ public List getStopPoints() {
+ return _stopPoints;
+ }
+
+ /**
+ * The angle of this linear gradient in compass degrees.
+ */
+ public float getAngle() {
+ return _angle;
+ }
+
+ public int getX1() {
+ return x1;
+ }
+
+ public int getX2() {
+ return x2;
+ }
+
+ public int getY1() {
+ return y1;
+ }
+
+ public int getY2() {
+ return y2;
+ }
+
+ @Override
+ public String toString() {
+ return "FSLinearGradient [_angle=" + _angle + ", _stopPoints=" + _stopPoints + ", x1=" + x1 + ", x2=" + x2
+ + ", y1=" + y1 + ", y2=" + y2 + "]";
+ }
+}
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java
index ac236caa2..7d90e173e 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/extend/OutputDevice.java
@@ -22,11 +22,15 @@
import com.openhtmltopdf.css.parser.FSColor;
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.derived.BorderPropertySet;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.render.*;
+import com.openhtmltopdf.util.XRLog;
+
import java.awt.*;
import java.awt.RenderingHints.Key;
import java.awt.geom.AffineTransform;
import java.util.List;
+import java.util.logging.Level;
public interface OutputDevice {
public void setPaint(Paint paint);
@@ -85,6 +89,10 @@ public void paintBackground(
public void drawImage(FSImage image, int x, int y, boolean interpolate);
+ default public void drawLinearGradient(FSLinearGradient backgroundLinearGradient, Shape bounds) {
+ XRLog.render(Level.WARNING, "linear-gradient(...) is not supported in this output device");
+ }
+
public void draw(Shape s);
public void fill(Shape s);
public void fillRect(int x, int y, int width, int height);
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java
index 224e1ae2a..f245207c8 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/render/AbstractOutputDevice.java
@@ -32,6 +32,7 @@
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.CssContext;
import com.openhtmltopdf.css.style.derived.BorderPropertySet;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.css.style.derived.LengthValue;
import com.openhtmltopdf.css.value.FontSpecification;
import com.openhtmltopdf.extend.FSImage;
@@ -243,7 +244,14 @@ private void paintBackground0(
}
FSColor backgroundColor = style.getBackgroundColor();
- FSImage backgroundImage = getBackgroundImage(c, style);
+ FSImage backgroundImage = null;
+ FSLinearGradient backgroundLinearGradient = null;
+
+ if (style.isLinearGradient()) {
+ backgroundLinearGradient = style.getLinearGradient(c, (int) (bgImageContainer.width - border.width()), (int) (bgImageContainer.height - border.height()));
+ } else {
+ backgroundImage = getBackgroundImage(c, style);
+ }
// If the image width or height is zero, then there's nothing to draw.
// Also prevents infinte loop when trying to tile an image with zero size.
@@ -252,7 +260,7 @@ private void paintBackground0(
}
if ( (backgroundColor == null || backgroundColor == FSRGBColor.TRANSPARENT) &&
- backgroundImage == null) {
+ backgroundImage == null && backgroundLinearGradient == null) {
return;
}
@@ -272,7 +280,7 @@ private void paintBackground0(
borderBounds.intersect(new Area(oldclip));
}
setClip(borderBounds);
- } else if (backgroundImage != null) {
+ } else if (backgroundImage != null || backgroundLinearGradient != null) {
pushClip(borderBounds != null ? borderBounds : borderBoundsShape);
}
@@ -281,7 +289,7 @@ private void paintBackground0(
fill(borderBounds != null ? borderBounds : borderBoundsShape);
}
- if (backgroundImage != null) {
+ if (backgroundImage != null || backgroundLinearGradient != null) {
Rectangle localBGImageContainer = bgImageContainer;
if (style.isFixedBackground()) {
localBGImageContainer = c.getViewportRectangle();
@@ -295,6 +303,7 @@ private void paintBackground0(
yoff += (int)border.top();
}
+ if (backgroundImage != null) {
scaleBackgroundImage(c, style, localBGImageContainer, backgroundImage);
float imageWidth = backgroundImage.getWidth();
@@ -341,12 +350,15 @@ private void paintBackground0(
yoff,
backgroundBounds.y + backgroundBounds.height, style.isImageRenderingInterpolate());
}
+ } // End background image painting.
+ } else if (backgroundLinearGradient != null) {
+ drawLinearGradient(backgroundLinearGradient, new Rectangle(xoff, yoff, bgImageContainer.width, bgImageContainer.height));
}
}
if (!c.isFastRenderer()) {
setClip(oldclip);
- } else if (backgroundImage != null) {
+ } else if (backgroundImage != null || backgroundLinearGradient != null) {
popClip();
}
}
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-439-linear-gradient.pdf b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-439-linear-gradient.pdf
new file mode 100644
index 000000000..903afe225
Binary files /dev/null and b/openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-439-linear-gradient.pdf differ
diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-439-linear-gradient.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-439-linear-gradient.html
new file mode 100644
index 000000000..9ac5d5211
--- /dev/null
+++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-439-linear-gradient.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
index 0dcbcf436..38e3cbd29 100644
--- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
+++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java
@@ -1034,7 +1034,16 @@ public void testIssue440TrailingWsAlignRight() throws IOException {
public void testIssue309ClassCastExceptionOnFloatTd() throws IOException {
assertTrue(vt.runTest("issue-309-classcastexception-on-float-td"));
}
-
+
+ /**
+ * Tests various linear gradients.
+ * https://github.com/danfickle/openhtmltopdf/issues/439
+ */
+ @Test
+ public void testIssue439LinearGradient() throws IOException {
+ assertTrue(vt.runTest("issue-439-linear-gradient"));
+ }
+
/**
* Tests that a font-face rule with multiple sources in different formats
* loads the truetype font only.
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/GradientHelper.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/GradientHelper.java
new file mode 100644
index 000000000..d40b1fcc2
--- /dev/null
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/GradientHelper.java
@@ -0,0 +1,153 @@
+package com.openhtmltopdf.pdfboxout;
+
+import java.util.List;
+import java.awt.geom.*;
+import java.awt.Rectangle;
+import java.awt.Shape;
+
+import com.openhtmltopdf.css.parser.FSRGBColor;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient.StopPoint;
+
+import org.apache.pdfbox.cos.COSArray;
+import org.apache.pdfbox.cos.COSBoolean;
+import org.apache.pdfbox.cos.COSDictionary;
+import org.apache.pdfbox.cos.COSFloat;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.common.function.PDFunctionType3;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShadingType2;
+
+public class GradientHelper {
+ /**
+ * This method is used for creating linear gradient with its components.
+ *
+ * @return shading for rendering linear gradient in PDF
+ */
+ public static PDShading createLinearGradient(PdfBoxFastOutputDevice od, AffineTransform transform, FSLinearGradient gradient, Shape bounds)
+ {
+ PDShadingType2 shading = new PDShadingType2(new COSDictionary());
+ shading.setShadingType(PDShading.SHADING_TYPE2);
+ shading.setColorSpace(PDDeviceRGB.INSTANCE);
+
+ Rectangle rect = bounds.getBounds();
+
+ Point2D ptStart = new Point2D.Float(gradient.getX1() + (float) rect.getMinX(), gradient.getY1() + (float) rect.getMinY());
+ Point2D ptEnd = new Point2D.Float(gradient.getX2() + (float) rect.getMinX(), gradient.getY2() + (float) rect.getMinY());
+
+ Point2D ptStartDevice = transform.transform(ptStart, null);
+ Point2D ptEndDevice = transform.transform(ptEnd, null);
+
+ float startX = (float) ptStartDevice.getX();
+ float startY = (float) od.normalizeY((float) ptStartDevice.getY());
+ float endX = (float) ptEndDevice.getX();
+ float endY = (float) od.normalizeY((float) ptEndDevice.getY());
+
+ COSArray coords = new COSArray();
+ coords.add(new COSFloat(startX));
+ coords.add(new COSFloat(startY));
+ coords.add(new COSFloat(endX));
+ coords.add(new COSFloat(endY));
+ shading.setCoords(coords);
+
+ PDFunctionType3 type3 = buildType3Function(gradient.getStopPoints(), (float) ptEnd.distance(ptStart));
+
+ COSArray extend = new COSArray();
+ extend.add(COSBoolean.FALSE);
+ extend.add(COSBoolean.FALSE);
+ shading.setFunction(type3);
+ shading.setExtend(extend);
+ return shading;
+ }
+
+ /**
+ * This method is used for setting colour lengths to linear gradient.
+ *
+ * @return the function, which is an important parameter for setting linear
+ * gradient.
+ * @param stopPoints
+ * colours and lengths of linear gradient.
+ */
+ private static PDFunctionType3 buildType3Function(List stopPoints, float distance) {
+ float max = stopPoints.get(stopPoints.size() - 1).getLength();
+
+ COSDictionary function = new COSDictionary();
+ function.setInt(COSName.FUNCTION_TYPE, 3);
+
+ COSArray domain = new COSArray();
+ domain.add(new COSFloat(0));
+ domain.add(new COSFloat(1));
+
+ COSArray encode = new COSArray();
+
+ COSArray range = new COSArray();
+ range.add(new COSFloat(0));
+ range.add(new COSFloat(1));
+ COSArray bounds = new COSArray();
+ for (int i = 1; i < stopPoints.size() - 1; i++) {
+ float pos = ((stopPoints.get(i).getLength() / max) * distance) * (1 / distance);
+ bounds.add(new COSFloat(pos));
+ }
+
+ COSArray functions = buildType2Functions(stopPoints, domain, encode);
+
+ function.setItem(COSName.FUNCTIONS, functions);
+ function.setItem(COSName.BOUNDS, bounds);
+ function.setItem(COSName.ENCODE, encode);
+ PDFunctionType3 type3 = new PDFunctionType3(function);
+ type3.setDomainValues(domain);
+ return type3;
+ }
+
+ /**
+ * This method is used for setting colours to linear gradient.
+ *
+ * @return the COSArray, which is an important parameter for setting linear
+ * gradient.
+ * @param stopPoints
+ * colours to use.
+ * @param domain
+ * parameter for setting functiontype2
+ * @param encode
+ * encoding COSArray
+ */
+ private static COSArray buildType2Functions(List stopPoints, COSArray domain, COSArray encode)
+ {
+ FSRGBColor prevColor = (FSRGBColor) stopPoints.get(0).getColor();
+
+ COSArray functions = new COSArray();
+ for (int i = 1; i < stopPoints.size(); i++)
+ {
+
+ FSRGBColor color = (FSRGBColor) stopPoints.get(i).getColor();
+
+ float[] component = new float[] { prevColor.getRed() / 255f, prevColor.getGreen() / 255f, prevColor.getBlue() / 255f };
+ PDColor prevPdColor = new PDColor(component, PDDeviceRGB.INSTANCE);
+
+ float[] component1 = new float[] { color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f };
+ PDColor pdColor = new PDColor(component1, PDDeviceRGB.INSTANCE);
+
+ COSArray c0 = new COSArray();
+ COSArray c1 = new COSArray();
+ for (float component2 : prevPdColor.getComponents())
+ c0.add(new COSFloat(component2));
+ for (float component3 : pdColor.getComponents())
+ c1.add(new COSFloat(component3));
+
+ COSDictionary type2Function = new COSDictionary();
+ type2Function.setInt(COSName.FUNCTION_TYPE, 2);
+ type2Function.setItem(COSName.C0, c0);
+ type2Function.setItem(COSName.C1, c1);
+ type2Function.setInt(COSName.N, 1);
+ type2Function.setItem(COSName.DOMAIN, domain);
+ functions.add(type2Function);
+
+ encode.add(new COSFloat(0));
+ encode.add(new COSFloat(1));
+ prevColor = color;
+ }
+ return functions;
+ }
+}
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
index 6f99092c7..81780793e 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastOutputDevice.java
@@ -27,6 +27,7 @@
import com.openhtmltopdf.css.parser.FSRGBColor;
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.CssContext;
+import com.openhtmltopdf.css.style.derived.FSLinearGradient;
import com.openhtmltopdf.css.value.FontSpecification;
import com.openhtmltopdf.extend.FSImage;
import com.openhtmltopdf.extend.OutputDevice;
@@ -35,7 +36,6 @@
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.outputdevice.helper.FontResolverHelper;
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver.FontDescription;
-import com.openhtmltopdf.pdfboxout.PdfBoxPerDocumentFormState;
import com.openhtmltopdf.pdfboxout.PdfBoxSlowOutputDevice.FontRun;
import com.openhtmltopdf.pdfboxout.PdfBoxSlowOutputDevice.Metadata;
import com.openhtmltopdf.render.*;
@@ -54,6 +54,7 @@
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -630,7 +631,7 @@ private void followPath(Shape s, GraphicsOperation drawType) {
/**
* Converts a top down unit to a bottom up PDF unit for the current page.
*/
- private float normalizeY(float y) {
+ public float normalizeY(float y) {
return _pageHeight - y;
}
@@ -800,6 +801,12 @@ public void realizeImage(PdfBoxImage img) {
img.setXObject(xobject);
}
+ @Override
+ public void drawLinearGradient(FSLinearGradient backgroundLinearGradient, Shape bounds) {
+ PDShading shading = GradientHelper.createLinearGradient(this, getTransform(), backgroundLinearGradient, bounds);
+ _cp.paintGradient(shading);
+ }
+
@Override
public void drawImage(FSImage fsImage, int x, int y, boolean interpolate) {
PdfBoxImage img = (PdfBoxImage) fsImage;
diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java
index 6c9ef5319..73e94783a 100644
--- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java
+++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfContentStreamAdapter.java
@@ -9,6 +9,7 @@
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.pdmodel.graphics.state.RenderingMode;
import org.apache.pdfbox.util.Matrix;
@@ -367,4 +368,12 @@ public void endMarkedContent() {
logAndThrow("endMarkedContent", e);
}
}
+
+ public void paintGradient(PDShading shading) {
+ try {
+ cs.shadingFill(shading);
+ } catch (IOException e) {
+ logAndThrow("paintGradient", e);
+ }
+ }
}