From a5dff7f3f86f6f6024ac979f866b92636397765a Mon Sep 17 00:00:00 2001 From: danfickle Date: Fri, 21 Feb 2020 21:27:27 +1100 Subject: [PATCH 1/3] #439 Start implementing linear gradient. [ci skip] --- .../property/AbstractPropertyBuilder.java | 4 + .../css/style/derived/FSLinearGradient.java | 247 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java 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/style/derived/FSLinearGradient.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java new file mode 100644 index 000000000..e0207305b --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/css/style/derived/FSLinearGradient.java @@ -0,0 +1,247 @@ +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; + + public StopPoint(FSColor color, float length) { + super(color); + this._length = length; + } + + public float getLength() { + return _length; + } + } + + private final List _stopPoints; + private final float _angle; + // TODO. + // private final int x1; + // private final int x2; + // private final int y1; + // private final int y2; + + public FSLinearGradient(CalculatedStyle style, FSFunction function, float boxWidth, 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); + } + + 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(); i++) { + 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)); + } else { + points.add(new IntermediateStopPoint(color)); + } + } + + 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; + + // TODO: Check for div by zero. + 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() == IntermediateStopPoint.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() && + 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 + 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; + } +} From 4a2bb3bd46b86af55bdbb84d961f81d71dcb2652 Mon Sep 17 00:00:00 2001 From: danfickle Date: Sun, 23 Feb 2020 22:03:48 +1100 Subject: [PATCH 2/3] #439 Linear gradient implementation for pdf output device. [ci skip] With test. Uses code from csspdfbox (LGPL) - thank you. --- .../property/PrimitivePropertyBuilders.java | 18 +++ .../css/style/CalculatedStyle.java | 21 ++- .../css/style/derived/FSLinearGradient.java | 153 ++++++++++++++++-- .../openhtmltopdf/extend/OutputDevice.java | 8 + .../render/AbstractOutputDevice.java | 22 ++- .../expected/issue-439-linear-gradient.pdf | Bin 0 -> 7512 bytes .../html/issue-439-linear-gradient.html | 55 +++++++ .../VisualRegressionTest.java | 10 +- .../pdfboxout/GradientHelper.java | 153 ++++++++++++++++++ .../pdfboxout/PdfBoxFastOutputDevice.java | 11 +- .../pdfboxout/PdfContentStreamAdapter.java | 9 ++ 11 files changed, 434 insertions(+), 26 deletions(-) create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/expected/issue-439-linear-gradient.pdf create mode 100644 openhtmltopdf-examples/src/main/resources/visualtest/html/issue-439-linear-gradient.html create mode 100644 openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/GradientHelper.java 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 index e0207305b..d386ab6de 100644 --- 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 @@ -37,7 +37,7 @@ public FSColor getColor() { public static class StopPoint extends IntermediateStopPoint { private final float _length; - public StopPoint(FSColor color, float length) { + StopPoint(FSColor color, float length) { super(color); this._length = length; } @@ -45,17 +45,22 @@ public StopPoint(FSColor color, float length) { public float getLength() { return _length; } + + @Override + public String toString() { + return "StopPoint [length=" + _length + + ", color=" + getColor() + "]"; + } } private final List _stopPoints; private final float _angle; - // TODO. - // private final int x1; - // private final int x2; - // private final int y1; - // private final int y2; + private int x1; + private int x2; + private int y1; + private int y2; - public FSLinearGradient(CalculatedStyle style, FSFunction function, float boxWidth, CssContext ctx) { + public FSLinearGradient(CalculatedStyle style, FSFunction function, int boxWidth, int boxHeight, CssContext ctx) { List params = function.getParameters(); int stopsStartIndex = getStopsStartIndex(params); @@ -67,6 +72,95 @@ public FSLinearGradient(CalculatedStyle style, FSFunction function, float boxWid 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) { @@ -79,7 +173,7 @@ private List calculateStopPoints( List points = new ArrayList<>(); - for (int i = stopsStartIndex; i < params.size(); i++) { + for (int i = stopsStartIndex; i < params.size();) { PropertyValue value = params.get(i); FSColor color; @@ -95,8 +189,10 @@ private List calculateStopPoints( 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; } } @@ -137,12 +233,12 @@ private List calculateStopPoints( int rangeCount = (topRangeIndex - bottomRangeIndex) + 1; int thisCount = i - bottomRangeIndex; - // TODO: Check for div by zero. - float interval = range / rangeCount; - - float thisLength = prevLength + (interval * thisCount); - - ret.add(new StopPoint(pt.getColor(), thisLength)); + // rangeCount should never be zero. + if (rangeCount != 0) { + float interval = range / rangeCount; + float thisLength = prevLength + (interval * thisCount); + ret.add(new StopPoint(pt.getColor(), thisLength)); + } } } @@ -164,7 +260,7 @@ private float get100PercentDefaultStopLength(CalculatedStyle style, CssContext c } private boolean isStopPointWithLength(IntermediateStopPoint pt) { - return pt.getClass() == IntermediateStopPoint.class; + return pt.getClass() == StopPoint.class; } private int getNextStopPointWithLengthIndex(List points, int startIndex) { @@ -180,6 +276,7 @@ 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++; } @@ -215,6 +312,10 @@ 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; } @@ -244,4 +345,26 @@ public List getStopPoints() { 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 0000000000000000000000000000000000000000..903afe225208e3da4c52b3f3299f137d465fd992 GIT binary patch literal 7512 zcmcIpOLH4V5WerPn2Re(QAqRJ*hlj{hYmgnGL`H{iYVf z(PTSWHdop3hkCtPG%GPwgCZNgn_Sf!p+WpSd-W=-S5s6oOntPjCtFY$PqwvqKK@Kg zEv42*t6a(A6Q!O>`RoN)S~t_%%X%%IH#hZazP(;<<8 zjkvIg`7sK*4a zxpUHDzG6t+R|jSAb}Z|2^-*MA2lV}BR%mFO9PGqNpGI%3WqBw0hPGm?`Ma&sh zP*wQY(v25l3|YpIs>=3ca1$&L8IB>@1))mDt8??tiL$pm6ln4u2+CLL&8H z;Ww3sVetnM-g8KZUvdySWN;q{eiM5b2=eP2cECfM1VkvEJ`nus^#~BYdj}A-NkD{} z?E}H@V~+qqeF{0D?wJ4xjiL_#Z(p&#sZp4=-k~Yzc!hlfS6J*4;OlgC*-UGHLsfJu z9XHpL#fm*?`;p>3@R%cNWN?4L4kI)bIrxfh;eBi?`ht6ASQYzN{EZoSfy=1q$HtrG zF>F-t49&h{cyf#c-Vu*EqGEo6BPzt$^^Q0)RIpER#CzK@M^u!;P+15=$;sTpv8uvU z_;wrQ*4Uz??$*h$c$)F&ak4dUb;qDlY73YYG2@I+Jh+m=E8jj;*g%y^Yb-uc+wB zH}DItV?_Y(uXZfB%K_Fbq=8fwaEr7(&dtHj7UJ-3Vtc?i(#U$_IM~q+66GB@TgbdD zF?}$!UOxxB(|;An!LD>Lkd(W=2&l{4*^Lqh9bO)uS>|9f>S3zmxi_AJo#npq-0$S6 zO&;Sd9)ma65|-9ZZ#aDw@7<(EA6_riV2?$UmR}o9EYaRL9$WDbJ^uR@e#a`0=bp!Z zzrug=x%}Vd|8ZT<;CJ^{W^(r>T%JQh!1M5h7g+oftN2Jc0G2+I1yGVcf~!#XkuhB< zUF=Ee!a)@$wscv*$tI8B?-yMVMpwIe76??YIyc>|M%I>3F7G1t_wJ)XjrA!HvPXZ#hWjE|R z-#ek)m0hTLQFe)j*X?<;-=_bF57^%>&aqSujvn$=S>5D z0>tSu8-B5xH3BC-T#v=Y%hH|d*Yb5aDxE2uk>yy9T&2%W?U^2p&g?7EF9IXQ`^6tM X)*Jm*Z?fJ}1v)2Xc5-t1?OFCO(eU3# literal 0 HcmV?d00001 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..4f585859d 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,15 @@ public void testIssue440TrailingWsAlignRight() throws IOException { public void testIssue309ClassCastExceptionOnFloatTd() throws IOException { assertTrue(vt.runTest("issue-309-classcastexception-on-float-td")); } - + + /** + * Tests various linear gradients. + */ + @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); + } + } } From 79bf73c13da194d5bab685f948bfa12689c9d1bc Mon Sep 17 00:00:00 2001 From: danfickle Date: Sun, 23 Feb 2020 22:18:02 +1100 Subject: [PATCH 3/3] #439 Meaningless commit to get ci tests to run. I thought they would run on PR creation. --- .../visualregressiontests/VisualRegressionTest.java | 1 + 1 file changed, 1 insertion(+) 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 4f585859d..38e3cbd29 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/visualregressiontests/VisualRegressionTest.java @@ -1037,6 +1037,7 @@ public void testIssue309ClassCastExceptionOnFloatTd() throws IOException { /** * Tests various linear gradients. + * https://github.com/danfickle/openhtmltopdf/issues/439 */ @Test public void testIssue439LinearGradient() throws IOException {