Skip to content

Commit eca659a

Browse files
committed
Support 'objectBoundingBox' value for SVG pattern element 'patternContentUnits' and 'patternUnits'
DEVSIX-4781
1 parent f45c866 commit eca659a

22 files changed

+235
-98
lines changed

svg/src/main/java/com/itextpdf/svg/renderers/impl/LinearGradientSvgNodeRenderer.java

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ This file is part of the iText (R) project.
2929
import com.itextpdf.kernel.geom.AffineTransform;
3030
import com.itextpdf.kernel.geom.Point;
3131
import com.itextpdf.kernel.geom.Rectangle;
32-
import com.itextpdf.styledxmlparser.css.util.CssDimensionParsingUtils;
33-
import com.itextpdf.styledxmlparser.css.util.CssTypesValidationUtils;
3432
import com.itextpdf.svg.SvgConstants.Attributes;
3533
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
3634
import com.itextpdf.svg.renderers.SvgDrawContext;
@@ -44,6 +42,9 @@ This file is part of the iText (R) project.
4442
*/
4543
public class LinearGradientSvgNodeRenderer extends AbstractGradientSvgNodeRenderer {
4644

45+
46+
private static final double CONVERT_COEFF = 0.75;
47+
4748
@Override
4849
public Color createColor(SvgDrawContext context, Rectangle objectBoundingBox, float objectBoundingBoxMargin,
4950
float parentOpacity) {
@@ -121,7 +122,8 @@ private AffineTransform getGradientTransformToUserSpaceOnUse(Rectangle objectBou
121122
// as we parse translate(1, 1) to translation(0.75, 0.75) the bounding box in
122123
// the gradient vector space should be 0.75x0.75 in order for such translation
123124
// to shift by the complete size of bounding box.
124-
gradientTransform.scale(objectBoundingBox.getWidth() / 0.75, objectBoundingBox.getHeight() / 0.75);
125+
gradientTransform
126+
.scale(objectBoundingBox.getWidth() / CONVERT_COEFF, objectBoundingBox.getHeight() / CONVERT_COEFF);
125127
}
126128

127129
AffineTransform svgGradientTransformation = getGradientTransform();
@@ -135,10 +137,17 @@ private Point[] getCoordinates(SvgDrawContext context, boolean isObjectBoundingB
135137
Point start;
136138
Point end;
137139
if (isObjectBoundingBox) {
138-
start = new Point(getCoordinateForObjectBoundingBox(Attributes.X1, 0),
139-
getCoordinateForObjectBoundingBox(Attributes.Y1, 0));
140-
end = new Point(getCoordinateForObjectBoundingBox(Attributes.X2, 1),
141-
getCoordinateForObjectBoundingBox(Attributes.Y2, 0));
140+
// need to multiply by 0.75 as further the (top, right) coordinates of the object bbox
141+
// would be transformed into (0.75, 0.75) point instead of (1, 1). The reason described
142+
// as a comment inside the method constructing the gradient transformation
143+
start = new Point(SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
144+
getAttribute(Attributes.X1), 0) * CONVERT_COEFF,
145+
SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
146+
getAttribute(Attributes.Y1), 0) * CONVERT_COEFF);
147+
end = new Point(SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
148+
getAttribute(Attributes.X2), 1) * CONVERT_COEFF,
149+
SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
150+
getAttribute(Attributes.Y2), 0) * CONVERT_COEFF);
142151
} else {
143152
Rectangle currentViewPort = context.getCurrentViewPort();
144153
double x = currentViewPort.getX();
@@ -161,35 +170,4 @@ private Point[] getCoordinates(SvgDrawContext context, boolean isObjectBoundingB
161170

162171
return new Point[] {start, end};
163172
}
164-
165-
private double getCoordinateForObjectBoundingBox(String attributeName, double defaultValue) {
166-
String attributeValue = getAttribute(attributeName);
167-
double absoluteValue = defaultValue;
168-
if (CssTypesValidationUtils.isPercentageValue(attributeValue)) {
169-
absoluteValue = CssDimensionParsingUtils.parseRelativeValue(attributeValue, 1);
170-
} else if (CssTypesValidationUtils.isNumericValue(attributeValue)
171-
|| CssTypesValidationUtils.isMetricValue(attributeValue)
172-
|| CssTypesValidationUtils.isRelativeValue(attributeValue)) {
173-
// if there is incorrect value metric, then we do not need to parse the value
174-
int unitsPosition = CssDimensionParsingUtils.determinePositionBetweenValueAndUnit(attributeValue);
175-
if (unitsPosition > 0) {
176-
// We want to ignore the unit type. From the svg specification:
177-
// "the normal of the linear gradient is perpendicular to the gradient vector in
178-
// object bounding box space (i.e., the abstract coordinate system where (0,0)
179-
// is at the top/left of the object bounding box and (1,1) is at the bottom/right
180-
// of the object bounding box)".
181-
// Different browsers treats this differently. We chose the "Google Chrome" approach
182-
// which treats the "abstract coordinate system" in the coordinate metric measure,
183-
// i.e. for value '0.5cm' the top/left of the object bounding box would be (1cm, 1cm),
184-
// for value '0.5em' the top/left of the object bounding box would be (1em, 1em) and etc.
185-
// no null pointer should be thrown as determine
186-
absoluteValue = CssDimensionParsingUtils.parseDouble(attributeValue.substring(0, unitsPosition)).doubleValue();
187-
}
188-
}
189-
190-
// need to multiply by 0.75 as further the (top, right) coordinates of the object bbox
191-
// would be transformed into (0.75, 0.75) point instead of (1, 1). The reason described
192-
// as a comment inside the method constructing the gradient transformation
193-
return absoluteValue * 0.75;
194-
}
195173
}

svg/src/main/java/com/itextpdf/svg/renderers/impl/PatternSvgNodeRenderer.java

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ This file is part of the iText (R) project.
6666
*/
6767
public class PatternSvgNodeRenderer extends AbstractBranchSvgNodeRenderer implements ISvgPaintServer {
6868

69-
private static final double ZERO = 1E-10;
69+
private static final double CONVERT_COEFF = 0.75;
7070

7171
@Override
7272
public ISvgNodeRenderer createDeepCopy() {
@@ -84,44 +84,66 @@ public Color createColor(SvgDrawContext context, Rectangle objectBoundingBox, fl
8484
return null;
8585
}
8686
try {
87-
PdfPattern.Tiling tilingPattern = createTilingPattern(context);
87+
PdfPattern.Tiling tilingPattern = createTilingPattern(context, objectBoundingBox);
8888
drawPatternContent(context, tilingPattern);
8989
return (tilingPattern == null) ? null : new PatternColor(tilingPattern);
9090
} finally {
9191
context.popPatternId();
9292
}
9393
}
9494

95-
private PdfPattern.Tiling createTilingPattern(SvgDrawContext context) {
95+
private PdfPattern.Tiling createTilingPattern(SvgDrawContext context,
96+
Rectangle objectBoundingBox) {
9697
final boolean isObjectBoundingBoxInPatternUnits = isObjectBoundingBoxInPatternUnits();
9798
final boolean isObjectBoundingBoxInPatternContentUnits = isObjectBoundingBoxInPatternContentUnits();
9899
final boolean isViewBoxExist = getAttribute(Attributes.VIEWBOX) != null;
100+
101+
// evaluate pattern rectangle on target pattern units
102+
Rectangle originalPatternRectangle = calculateOriginalPatternRectangle(
103+
context, isObjectBoundingBoxInPatternUnits);
104+
105+
// get xStep and yStep on target pattern units
106+
double xStep = originalPatternRectangle.getWidth();
107+
double yStep = originalPatternRectangle.getHeight();
108+
109+
if (xStep == 0 || yStep == 0) {
110+
return null;
111+
}
112+
113+
// transform user space to target pattern rectangle origin and scale
99114
final AffineTransform patternAffineTransform = new AffineTransform();
100-
double xOffset, yOffset, xStep, yStep;
101-
Rectangle bbox;
102-
if (isObjectBoundingBoxInPatternUnits || isViewBoxExist || isObjectBoundingBoxInPatternContentUnits) {
115+
if (isObjectBoundingBoxInPatternUnits) {
116+
patternAffineTransform.concatenate(getTransformToUserSpaceOnUse(objectBoundingBox));
117+
}
118+
patternAffineTransform.translate(originalPatternRectangle.getX(), originalPatternRectangle.getY());
119+
120+
double bboxWidth, bboxHeight;
121+
if (isViewBoxExist) {
122+
// TODO: DEVSIX-4782 support 'viewbox' and `preserveAspectRatio' attribute for SVG pattern element
103123
return null;
104124
} else {
105-
final Rectangle currentViewPort = context.getCurrentViewPort();
106-
final double viewPortX = currentViewPort.getX();
107-
final double viewPortY = currentViewPort.getY();
108-
final double viewPortWidth = currentViewPort.getWidth();
109-
final double viewPortHeight = currentViewPort.getHeight();
110-
final float em = getCurrentFontSize();
111-
final float rem = context.getCssContext().getRootFontSize();
112-
xOffset = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
113-
getAttribute(Attributes.X), viewPortX, viewPortX, viewPortWidth, em, rem);
114-
yOffset = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
115-
getAttribute(Attributes.Y), viewPortY, viewPortY, viewPortHeight, em, rem);
116-
xStep = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
117-
getAttribute(Attributes.WIDTH), viewPortX, viewPortX, viewPortWidth, em, rem);
118-
yStep = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
119-
getAttribute(Attributes.HEIGHT), viewPortX, viewPortX, viewPortHeight, em, rem);
120-
bbox = new Rectangle(0F, 0F, (float) xStep, (float) yStep);
121-
if (!isZero(xOffset, ZERO) || !isZero(yOffset, ZERO)) {
122-
patternAffineTransform.translate(xOffset, yOffset);
125+
if (isObjectBoundingBoxInPatternUnits != isObjectBoundingBoxInPatternContentUnits) {
126+
// If pattern units are not the same as pattern content units, then we need to scale
127+
// the resulted space into a space to draw pattern content. The pattern rectangle origin
128+
// is already in place, but measures should be adjusted.
129+
double scaleX, scaleY;
130+
if (isObjectBoundingBoxInPatternContentUnits) {
131+
scaleX = objectBoundingBox.getWidth() / CONVERT_COEFF;
132+
scaleY = objectBoundingBox.getHeight() / CONVERT_COEFF;
133+
} else {
134+
scaleX = CONVERT_COEFF / objectBoundingBox.getWidth();
135+
scaleY = CONVERT_COEFF / objectBoundingBox.getHeight();
136+
}
137+
patternAffineTransform.concatenate(AffineTransform.getScaleInstance(scaleX, scaleY));
138+
xStep /= scaleX;
139+
yStep /= scaleY;
123140
}
141+
bboxWidth = xStep;
142+
bboxHeight = yStep;
124143
}
144+
145+
Rectangle bbox = new Rectangle(0F, 0F, (float) bboxWidth, (float) bboxHeight);
146+
125147
return createColoredTilingPatternInstance(patternAffineTransform, bbox, xStep, yStep);
126148
}
127149

@@ -157,6 +179,39 @@ private void setPatternMatrix(PdfPattern.Tiling pattern, AffineTransform affineT
157179
}
158180
}
159181

182+
private Rectangle calculateOriginalPatternRectangle(SvgDrawContext context,
183+
boolean isObjectBoundingBoxInPatternUnits) {
184+
double xOffset, yOffset, xStep, yStep;
185+
if (isObjectBoundingBoxInPatternUnits) {
186+
xOffset = SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
187+
getAttribute(Attributes.X), 0) * CONVERT_COEFF;
188+
yOffset = SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
189+
getAttribute(Attributes.Y), 0) * CONVERT_COEFF;
190+
xStep = SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
191+
getAttribute(Attributes.WIDTH), 0) * CONVERT_COEFF;
192+
yStep = SvgCoordinateUtils.getCoordinateForObjectBoundingBox(
193+
getAttribute(Attributes.HEIGHT), 0) * CONVERT_COEFF;
194+
} else {
195+
final Rectangle currentViewPort = context.getCurrentViewPort();
196+
final double viewPortX = currentViewPort.getX();
197+
final double viewPortY = currentViewPort.getY();
198+
final double viewPortWidth = currentViewPort.getWidth();
199+
final double viewPortHeight = currentViewPort.getHeight();
200+
final float em = getCurrentFontSize();
201+
final float rem = context.getCssContext().getRootFontSize();
202+
// get pattern coordinates in userSpaceOnUse coordinate system
203+
xOffset = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
204+
getAttribute(Attributes.X), viewPortX, viewPortX, viewPortWidth, em, rem);
205+
yOffset = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
206+
getAttribute(Attributes.Y), viewPortY, viewPortY, viewPortHeight, em, rem);
207+
xStep = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
208+
getAttribute(Attributes.WIDTH), viewPortX, viewPortX, viewPortWidth, em, rem);
209+
yStep = SvgCoordinateUtils.getCoordinateForUserSpaceOnUse(
210+
getAttribute(Attributes.HEIGHT), viewPortY, viewPortY, viewPortHeight, em, rem);
211+
}
212+
return new Rectangle((float) xOffset, (float) yOffset, (float) xStep, (float) yStep);
213+
}
214+
160215
private boolean isObjectBoundingBoxInPatternUnits() {
161216
final String patternUnits = getAttribute(Attributes.PATTERN_UNITS);
162217
if (Values.USER_SPACE_ON_USE.equals(patternUnits)) {
@@ -180,7 +235,11 @@ private boolean isObjectBoundingBoxInPatternContentUnits() {
180235
return false;
181236
}
182237

183-
private static boolean isZero(double val, double delta) {
184-
return -delta < val && val < delta;
238+
private AffineTransform getTransformToUserSpaceOnUse(Rectangle objectBoundingBox) {
239+
AffineTransform transform = new AffineTransform();
240+
transform.translate(objectBoundingBox.getX(), objectBoundingBox.getY());
241+
transform.scale(objectBoundingBox.getWidth() / CONVERT_COEFF,
242+
objectBoundingBox.getHeight() / CONVERT_COEFF);
243+
return transform;
185244
}
186245
}

svg/src/main/java/com/itextpdf/svg/utils/SvgCoordinateUtils.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ This file is part of the iText (R) project.
4444

4545
import com.itextpdf.kernel.geom.Vector;
4646
import com.itextpdf.layout.property.UnitValue;
47+
import com.itextpdf.styledxmlparser.css.util.CssDimensionParsingUtils;
48+
import com.itextpdf.styledxmlparser.css.util.CssTypesValidationUtils;
4749
import com.itextpdf.styledxmlparser.css.util.CssUtils;
4850
import com.itextpdf.svg.exceptions.SvgExceptionMessageConstant;
4951

@@ -92,11 +94,11 @@ public static double calculateAngleBetweenTwoVectors(Vector vectorA, Vector vect
9294
* Returns absolute value for attribute in userSpaceOnUse coordinate system.
9395
*
9496
* @param attributeValue value of attribute.
95-
* @param defaultValue default value.
96-
* @param start start border for calculating percent value.
97-
* @param length length for calculating percent value.
98-
* @param em em value.
99-
* @param rem rem value.
97+
* @param defaultValue default value.
98+
* @param start start border for calculating percent value.
99+
* @param length length for calculating percent value.
100+
* @param em em value.
101+
* @param rem rem value.
100102
* @return absolute value in the userSpaceOnUse coordinate system.
101103
*/
102104
public static double getCoordinateForUserSpaceOnUse(String attributeValue, double defaultValue,
@@ -112,4 +114,34 @@ public static double getCoordinateForUserSpaceOnUse(String attributeValue, doubl
112114
}
113115
return absoluteValue;
114116
}
117+
118+
/**
119+
* Returns a value relative to the object bounding box.
120+
* We should only call this method for attributes with coordinates relative to the object bounding rectangle.
121+
*
122+
* @param attributeValue attribute value to parse
123+
* @param defaultValue this value will be returned if an error occurs while parsing the attribute value
124+
* @return if {@code attributeValue} is a percentage value, the given percentage of 1 will be returned.
125+
* And if it's a valid value with a number, the number will be extracted from that value.
126+
*/
127+
public static double getCoordinateForObjectBoundingBox(String attributeValue, double defaultValue) {
128+
if (CssTypesValidationUtils.isPercentageValue(attributeValue)) {
129+
return CssDimensionParsingUtils.parseRelativeValue(attributeValue, 1);
130+
} else if (CssTypesValidationUtils.isNumericValue(attributeValue)
131+
|| CssTypesValidationUtils.isMetricValue(attributeValue)
132+
|| CssTypesValidationUtils.isRelativeValue(attributeValue)) {
133+
// if there is incorrect value metric, then we do not need to parse the value
134+
int unitsPosition = CssDimensionParsingUtils.determinePositionBetweenValueAndUnit(attributeValue);
135+
if (unitsPosition > 0) {
136+
// We want to ignore the unit type how this is done in the "Google Chrome" approach
137+
// which treats the "abstract coordinate system" in the coordinate metric measure,
138+
// i.e. for value '0.5cm' the top/left of the object bounding box would be (1cm, 1cm),
139+
// for value '0.5em' the top/left of the object bounding box would be (1em, 1em) and etc.
140+
// no null pointer should be thrown as determine
141+
return CssDimensionParsingUtils.parseDouble(attributeValue.substring(0, unitsPosition))
142+
.doubleValue();
143+
}
144+
}
145+
return defaultValue;
146+
}
115147
}

0 commit comments

Comments
 (0)