com.openhtmltopdf.css.style.derived.FSLinearGradient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openhtmltopdf-core Show documentation
Show all versions of openhtmltopdf-core Show documentation
Open HTML to PDF is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code.
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 + "]";
}
}