Skip to content

Commit 4f2b6a0

Browse files
committed
Support stroke-dasharray and stroke-dashoffset css attributes
For now only absolute values are supported. Percents are not supported. DEVSIX-4043
1 parent 9f0e824 commit 4f2b6a0

14 files changed

+374
-41
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2023 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.svg.css;
24+
25+
import com.itextpdf.styledxmlparser.css.util.CssDimensionParsingUtils;
26+
import com.itextpdf.styledxmlparser.css.util.CssTypesValidationUtils;
27+
import com.itextpdf.svg.SvgConstants;
28+
import com.itextpdf.svg.logs.SvgLogMessageConstant;
29+
import com.itextpdf.svg.utils.SvgCssUtils;
30+
31+
import java.util.ArrayList;
32+
import java.util.Arrays;
33+
import java.util.List;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
37+
/**
38+
* This class converts stroke related SVG parameters and attributes into those from PDF specification.
39+
*/
40+
public final class SvgStrokeParameterConverter {
41+
42+
private SvgStrokeParameterConverter() {
43+
}
44+
45+
private final static Logger LOGGER = LoggerFactory.getLogger(SvgStrokeParameterConverter.class);
46+
47+
/**
48+
* Convert stroke related SVG parameters and attributes into PDF line dash parameters.
49+
*
50+
* @param strokeDashArray 'stroke-dasharray' css property value.
51+
* @param strokeDashOffset 'stroke-dashoffset' css property value.
52+
* @return PDF line dash parameters represented by {@link PdfLineDashParameters}.
53+
*/
54+
public static PdfLineDashParameters convertStrokeDashParameters(String strokeDashArray, String strokeDashOffset) {
55+
if (strokeDashArray != null && !SvgConstants.Values.NONE.equalsIgnoreCase(strokeDashArray)) {
56+
List<String> dashArray = SvgCssUtils.splitValueList(strokeDashArray);
57+
58+
for (String dashArrayItem : dashArray) {
59+
if (CssTypesValidationUtils.isPercentageValue(dashArrayItem)) {
60+
LOGGER.error(SvgLogMessageConstant.
61+
PERCENTAGE_VALUES_IN_STROKE_DASHARRAY_AND_STROKE_DASHOFFSET_ARE_NOT_SUPPORTED);
62+
return null;
63+
}
64+
}
65+
66+
if (dashArray.size() > 0) {
67+
if (dashArray.size() % 2 == 1) {
68+
// If an odd number of values is provided, then the list of values is repeated to yield an even
69+
// number of values. Thus, 5,3,2 is equivalent to 5,3,2,5,3,2.
70+
dashArray.addAll(new ArrayList<>(dashArray));
71+
}
72+
float[] dashArrayFloat = new float[dashArray.size()];
73+
for (int i = 0; i < dashArray.size(); i++) {
74+
dashArrayFloat[i] = CssDimensionParsingUtils.parseAbsoluteLength(dashArray.get(i));
75+
}
76+
77+
// Parse stroke dash offset
78+
float dashPhase = 0;
79+
if (strokeDashOffset != null && !strokeDashOffset.isEmpty() &&
80+
!SvgConstants.Values.NONE.equalsIgnoreCase(strokeDashOffset)) {
81+
if (CssTypesValidationUtils.isPercentageValue(strokeDashOffset)) {
82+
LOGGER.error(SvgLogMessageConstant.
83+
PERCENTAGE_VALUES_IN_STROKE_DASHARRAY_AND_STROKE_DASHOFFSET_ARE_NOT_SUPPORTED);
84+
} else {
85+
dashPhase = CssDimensionParsingUtils.parseAbsoluteLength(strokeDashOffset);
86+
}
87+
}
88+
89+
return new PdfLineDashParameters(dashArrayFloat, dashPhase);
90+
}
91+
}
92+
93+
return null;
94+
}
95+
96+
/**
97+
* This class represents PDF dash parameters.
98+
*/
99+
public static class PdfLineDashParameters {
100+
private final float[] dashArray;
101+
private final float dashPhase;
102+
103+
/**
104+
* Construct PDF dash parameters.
105+
*
106+
* @param dashArray Numbers that specify the lengths of alternating dashes and gaps;
107+
* the numbers shall be nonnegative and not all zero.
108+
* @param dashPhase A number that specifies the distance into the dash pattern at which to start the dash.
109+
*/
110+
public PdfLineDashParameters(float[] dashArray, float dashPhase) {
111+
this.dashArray = dashArray;
112+
this.dashPhase = dashPhase;
113+
}
114+
115+
/**
116+
* Return dash array.
117+
*
118+
* @return dash array.
119+
*/
120+
public float[] getDashArray() {
121+
return dashArray;
122+
}
123+
124+
/**
125+
* Return dash phase.
126+
*
127+
* @return dash phase.
128+
*/
129+
public float getDashPhase() {
130+
return dashPhase;
131+
}
132+
133+
/**
134+
* Check if some object is equal to the given object.
135+
*/
136+
@Override
137+
public boolean equals(Object o) {
138+
if (this == o) {
139+
return true;
140+
}
141+
if (o == null || getClass() != o.getClass()) {
142+
return false;
143+
}
144+
145+
PdfLineDashParameters that = (PdfLineDashParameters) o;
146+
147+
if (Float.compare(that.dashPhase, dashPhase) != 0) {
148+
return false;
149+
}
150+
return Arrays.equals(dashArray, that.dashArray);
151+
}
152+
153+
/**
154+
* Generate a hash code for this object.
155+
*
156+
* @return hash code.
157+
*/
158+
@Override
159+
public int hashCode() {
160+
int result = Arrays.hashCode(dashArray);
161+
result = 31 * result + Float.floatToIntBits(dashPhase);
162+
return result;
163+
}
164+
}
165+
}

svg/src/main/java/com/itextpdf/svg/logs/SvgLogMessageConstant.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public final class SvgLogMessageConstant {
6161
public static final String PATTERN_WIDTH_OR_HEIGHT_IS_NEGATIVE =
6262
"Pattern width or height is negative value. This pattern will not be rendered.";
6363

64+
public static final String PERCENTAGE_VALUES_IN_STROKE_DASHARRAY_AND_STROKE_DASHOFFSET_ARE_NOT_SUPPORTED =
65+
"Percentage values in 'stroke-dasharray' and 'stroke-dashoffset' attributes are not supported. "
66+
+ "Attribute will be ignored completely.";
67+
6468
public static final String MISSING_WIDTH =
6569
"Top Svg tag has no defined width attribute and viewbox width is not present, so browser default of 300px "
6670
+ "is used";

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

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ This file is part of the iText (R) project.
2525
import com.itextpdf.kernel.colors.Color;
2626
import com.itextpdf.kernel.colors.ColorConstants;
2727
import com.itextpdf.kernel.colors.DeviceRgb;
28-
import com.itextpdf.kernel.colors.WebColors;
2928
import com.itextpdf.kernel.geom.AffineTransform;
3029
import com.itextpdf.kernel.geom.Rectangle;
3130
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
@@ -42,6 +41,8 @@ This file is part of the iText (R) project.
4241
import com.itextpdf.styledxmlparser.css.validate.CssDeclarationValidationMaster;
4342
import com.itextpdf.svg.MarkerVertexType;
4443
import com.itextpdf.svg.SvgConstants;
44+
import com.itextpdf.svg.SvgConstants.Attributes;
45+
import com.itextpdf.svg.css.SvgStrokeParameterConverter;
4546
import com.itextpdf.svg.css.impl.SvgNodeRendererInheritanceResolver;
4647
import com.itextpdf.svg.renderers.IMarkerCapable;
4748
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
@@ -300,9 +301,9 @@ void preDraw(SvgDrawContext context) {
300301

301302
PdfExtGState opacityGraphicsState = new PdfExtGState();
302303
if (!partOfClipPath) {
303-
float generalOpacity = getOpacity();
304304
// fill
305305
{
306+
float generalOpacity = getOpacity();
306307
String fillRawValue = getAttributeOrDefault(SvgConstants.Attributes.FILL, "black");
307308
this.doFill = !SvgConstants.Values.NONE.equalsIgnoreCase(fillRawValue);
308309

@@ -329,47 +330,9 @@ void preDraw(SvgDrawContext context) {
329330
currentCanvas.setFillColor(fillColor);
330331
}
331332
}
332-
// stroke
333-
{
334-
String strokeRawValue = getAttributeOrDefault(SvgConstants.Attributes.STROKE,
335-
SvgConstants.Values.NONE);
336-
337-
if (!SvgConstants.Values.NONE.equalsIgnoreCase(strokeRawValue)) {
338-
String strokeWidthRawValue = getAttribute(SvgConstants.Attributes.STROKE_WIDTH);
339-
340-
// 1 px = 0,75 pt
341-
float strokeWidth = 0.75f;
342-
343-
if (strokeWidthRawValue != null) {
344-
strokeWidth = CssDimensionParsingUtils.parseAbsoluteLength(strokeWidthRawValue);
345-
}
346333

347-
float strokeOpacity = getOpacityByAttributeName(SvgConstants.Attributes.STROKE_OPACITY,
348-
generalOpacity);
349-
350-
Color strokeColor = null;
351-
TransparentColor transparentColor = getColorFromAttributeValue(
352-
context, strokeRawValue, (float) ((double) strokeWidth / 2.0), strokeOpacity);
353-
if (transparentColor != null) {
354-
strokeColor = transparentColor.getColor();
355-
strokeOpacity = transparentColor.getOpacity();
356-
}
334+
applyStrokeProperties(context, currentCanvas, opacityGraphicsState);
357335

358-
if (!CssUtils.compareFloats(strokeOpacity, 1f)) {
359-
opacityGraphicsState.setStrokeOpacity(strokeOpacity);
360-
}
361-
362-
// as default value for stroke is 'none' we should not set
363-
// it in case when value obtaining fails
364-
if (strokeColor != null) {
365-
currentCanvas.setStrokeColor(strokeColor);
366-
}
367-
368-
currentCanvas.setLineWidth(strokeWidth);
369-
370-
doStroke = true;
371-
}
372-
}
373336
// opacity
374337
{
375338
if (!opacityGraphicsState.getPdfObject().isEmpty()) {
@@ -487,4 +450,55 @@ private float getOpacity() {
487450

488451
return result;
489452
}
453+
454+
private void applyStrokeProperties(SvgDrawContext context, PdfCanvas currentCanvas,
455+
PdfExtGState opacityGraphicsState) {
456+
String strokeRawValue = getAttributeOrDefault(SvgConstants.Attributes.STROKE,
457+
SvgConstants.Values.NONE);
458+
if (!SvgConstants.Values.NONE.equalsIgnoreCase(strokeRawValue)) {
459+
String strokeWidthRawValue = getAttribute(SvgConstants.Attributes.STROKE_WIDTH);
460+
461+
// 1 px = 0,75 pt
462+
float strokeWidth = 0.75f;
463+
464+
if (strokeWidthRawValue != null) {
465+
strokeWidth = CssDimensionParsingUtils.parseAbsoluteLength(strokeWidthRawValue);
466+
}
467+
468+
float generalOpacity = getOpacity();
469+
float strokeOpacity = getOpacityByAttributeName(SvgConstants.Attributes.STROKE_OPACITY,
470+
generalOpacity);
471+
472+
Color strokeColor = null;
473+
TransparentColor transparentColor = getColorFromAttributeValue(
474+
context, strokeRawValue, (float) ((double) strokeWidth / 2.0), strokeOpacity);
475+
if (transparentColor != null) {
476+
strokeColor = transparentColor.getColor();
477+
strokeOpacity = transparentColor.getOpacity();
478+
}
479+
480+
if (!CssUtils.compareFloats(strokeOpacity, 1f)) {
481+
opacityGraphicsState.setStrokeOpacity(strokeOpacity);
482+
}
483+
484+
String strokeDashArrayRawValue = getAttribute(Attributes.STROKE_DASHARRAY);
485+
String strokeDashOffsetRawValue = getAttribute(Attributes.STROKE_DASHOFFSET);
486+
SvgStrokeParameterConverter.PdfLineDashParameters lineDashParameters =
487+
SvgStrokeParameterConverter.convertStrokeDashParameters(strokeDashArrayRawValue,
488+
strokeDashOffsetRawValue);
489+
if (lineDashParameters != null) {
490+
currentCanvas.setLineDash(lineDashParameters.getDashArray(), lineDashParameters.getDashPhase());
491+
}
492+
493+
// as default value for stroke is 'none' we should not set
494+
// it in case when value obtaining fails
495+
if (strokeColor != null) {
496+
currentCanvas.setStrokeColor(strokeColor);
497+
}
498+
499+
currentCanvas.setLineWidth(strokeWidth);
500+
501+
doStroke = true;
502+
}
503+
}
490504
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2023 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.svg.css;
24+
25+
import com.itextpdf.svg.css.SvgStrokeParameterConverter.PdfLineDashParameters;
26+
import com.itextpdf.svg.logs.SvgLogMessageConstant;
27+
import com.itextpdf.test.ExtendedITextTest;
28+
import com.itextpdf.test.annotations.LogMessage;
29+
import com.itextpdf.test.annotations.LogMessages;
30+
import com.itextpdf.test.annotations.type.UnitTest;
31+
32+
import org.junit.Assert;
33+
import org.junit.Test;
34+
import org.junit.experimental.categories.Category;
35+
36+
@Category(UnitTest.class)
37+
public class SvgStrokeParameterConverterUnitTest extends ExtendedITextTest {
38+
39+
@LogMessages(messages = {
40+
@LogMessage(messageTemplate =
41+
SvgLogMessageConstant.PERCENTAGE_VALUES_IN_STROKE_DASHARRAY_AND_STROKE_DASHOFFSET_ARE_NOT_SUPPORTED)})
42+
@Test
43+
public void testStrokeDashArrayPercentsAreNotSupported() {
44+
Assert.assertNull(SvgStrokeParameterConverter.convertStrokeDashParameters("5,3%", null));
45+
}
46+
47+
@Test
48+
public void testStrokeDashArrayOddNumberOfValues() {
49+
PdfLineDashParameters result = SvgStrokeParameterConverter.convertStrokeDashParameters("5pt", null);
50+
Assert.assertNotNull(result);
51+
Assert.assertEquals(0, result.getDashPhase(), 0);
52+
Assert.assertArrayEquals(new float[] {5, 5}, result.getDashArray(), 1e-5f);
53+
}
54+
55+
@Test
56+
public void testEmptyStrokeDashArray() {
57+
PdfLineDashParameters result = SvgStrokeParameterConverter.convertStrokeDashParameters("", null);
58+
Assert.assertNull(result);
59+
}
60+
61+
@LogMessages(messages = {
62+
@LogMessage(messageTemplate =
63+
SvgLogMessageConstant.PERCENTAGE_VALUES_IN_STROKE_DASHARRAY_AND_STROKE_DASHOFFSET_ARE_NOT_SUPPORTED)})
64+
@Test
65+
public void testStrokeDashOffsetPercentsAreNotSupported() {
66+
PdfLineDashParameters result = SvgStrokeParameterConverter.convertStrokeDashParameters("5pt,3pt", "10%");
67+
Assert.assertEquals(new PdfLineDashParameters(new float[]{5, 3}, 0), result);
68+
}
69+
70+
@Test
71+
public void testEmptyStrokeDashOffset() {
72+
PdfLineDashParameters result = SvgStrokeParameterConverter.convertStrokeDashParameters("5pt,3pt", "");
73+
Assert.assertEquals(new PdfLineDashParameters(new float[]{5, 3}, 0), result);
74+
}
75+
76+
@Test
77+
public void testStrokeDashOffset() {
78+
PdfLineDashParameters result = SvgStrokeParameterConverter.convertStrokeDashParameters("5pt,3pt", "10");
79+
Assert.assertEquals(new PdfLineDashParameters(new float[]{5, 3}, 7.5f), result);
80+
}
81+
}

0 commit comments

Comments
 (0)