Skip to content

Commit 448b6f3

Browse files
committed
SVG: Support relative x and y resolving in text element
1 parent 2cb9f37 commit 448b6f3

File tree

8 files changed

+141
-42
lines changed

8 files changed

+141
-42
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,22 @@ public interface ISvgTextNodeRenderer extends ISvgNodeRenderer {
5151
@Deprecated
5252
boolean containsRelativeMove();
5353

54+
/**
55+
* This method is deprecated and will be replaced with new signature {@code containsAbsolutePositionChange(SvgDrawContext)}.
56+
* This is needed because x/y can contain relative values, so SvgDrawContext is needed to resolve them.
57+
*
58+
* @return {@code true} if an absolute position is specified via x/y attributes, {@code false} otherwise
59+
*/
60+
@Deprecated
5461
boolean containsAbsolutePositionChange();
5562

63+
/**
64+
* This method is deprecated and will be replaced with new signature {@code getAbsolutePositionChanges(SvgDrawContext)}.
65+
* This is needed because x/y can contain relative values, so SvgDrawContext is needed to resolve them.
66+
*
67+
* @return text absolute position
68+
*/
69+
@Deprecated
5670
float[][] getAbsolutePositionChanges();
5771

5872
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,13 @@ public boolean containsRelativeMove() {
8282
}
8383

8484
@Override
85+
@Deprecated
8586
public boolean containsAbsolutePositionChange() {
8687
return false; //Leaf text elements do not contain any kind of transformation
8788
}
8889

8990
@Override
91+
@Deprecated
9092
public float[][] getAbsolutePositionChanges() {
9193
float[] part = new float[]{0f};
9294
return new float[][]{part, part};

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

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.svg.renderers.impl;
2424

25+
import com.itextpdf.commons.utils.MessageFormatUtil;
2526
import com.itextpdf.kernel.font.PdfFont;
2627
import com.itextpdf.kernel.geom.AffineTransform;
2728
import com.itextpdf.kernel.geom.Point;
@@ -43,6 +44,7 @@ This file is part of the iText (R) project.
4344
import com.itextpdf.layout.renderer.ParagraphRenderer;
4445
import com.itextpdf.styledxmlparser.css.util.CssDimensionParsingUtils;
4546
import com.itextpdf.styledxmlparser.css.util.CssUtils;
47+
import com.itextpdf.styledxmlparser.exceptions.StyledXMLParserException;
4648
import com.itextpdf.svg.SvgConstants;
4749
import com.itextpdf.svg.css.SvgStrokeParameterConverter.PdfLineDashParameters;
4850
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
@@ -127,7 +129,7 @@ public float[] getRelativeTranslation() {
127129

128130
public float[] getRelativeTranslation(SvgDrawContext context) {
129131
if (!moveResolved) {
130-
resolveTextMove(context);
132+
resolveRelativeTextMove(context);
131133
}
132134
return new float[]{xMove, yMove};
133135
}
@@ -140,21 +142,33 @@ public boolean containsRelativeMove() {
140142

141143
public boolean containsRelativeMove(SvgDrawContext context) {
142144
if (!moveResolved) {
143-
resolveTextMove(context);
145+
resolveRelativeTextMove(context);
144146
}
145147
boolean isNullMove = CssUtils.compareFloats(0f, xMove) && CssUtils.compareFloats(0f, yMove); // comparison to 0
146148
return !isNullMove;
147149
}
148150

149151
@Override
150152
public boolean containsAbsolutePositionChange() {
151-
if (!posResolved) resolveTextPosition();
153+
return containsAbsolutePositionChange(new SvgDrawContext(null, null));
154+
}
155+
156+
public boolean containsAbsolutePositionChange(SvgDrawContext context) {
157+
if (!posResolved) {
158+
resolveAbsoluteTextPosition(context);
159+
}
152160
return (xPos != null && xPos.length > 0) || (yPos != null && yPos.length > 0);
153161
}
154162

155163
@Override
156164
public float[][] getAbsolutePositionChanges() {
157-
if (!posResolved) resolveTextPosition();
165+
return getAbsolutePositionChanges(new SvgDrawContext(null, null));
166+
}
167+
168+
public float[][] getAbsolutePositionChanges(SvgDrawContext context) {
169+
if (!posResolved) {
170+
resolveAbsoluteTextPosition(context);
171+
}
158172
return new float[][]{xPos, yPos};
159173
}
160174

@@ -178,12 +192,13 @@ public TextRectangle getTextRectangle(SvgDrawContext context, Point startPoint)
178192
// building of properly positioned rectangles without any drawing or visual properties applying.
179193
for (ISvgTextNodeRenderer child : children) {
180194
if (child instanceof TextSvgBranchRenderer) {
181-
startPoint = ((TextSvgBranchRenderer) child).getStartPoint(context, startPoint);
182-
if (child.containsAbsolutePositionChange() && textChunkRect != null) {
195+
TextSvgBranchRenderer childText = (TextSvgBranchRenderer) child;
196+
startPoint = childText.getStartPoint(context, startPoint);
197+
if (childText.containsAbsolutePositionChange(context) && textChunkRect != null) {
183198
commonRect = getCommonRectangleWithAnchor(commonRect, textChunkRect, rootX, textAnchorValue);
184199
// Start new text chunk.
185200
textChunkRect = null;
186-
textAnchorValue = child.getAttribute(SvgConstants.Attributes.TEXT_ANCHOR);
201+
textAnchorValue = childText.getAttribute(SvgConstants.Attributes.TEXT_ANCHOR);
187202
rootX = (float) startPoint.getX();
188203
}
189204
} else {
@@ -208,7 +223,7 @@ public Rectangle getObjectBoundingBox(SvgDrawContext context) {
208223
if (objectBoundingBox == null) {
209224
// Handle white-spaces
210225
if (!whiteSpaceProcessed) {
211-
SvgTextUtil.processWhiteSpace(this, true);
226+
SvgTextUtil.processWhiteSpace(this, true, context);
212227
}
213228
objectBoundingBox = getTextRectangle(context, null);
214229
}
@@ -235,7 +250,7 @@ protected void doDraw(SvgDrawContext context) {
235250
}
236251
// Handle white-spaces
237252
if (!whiteSpaceProcessed) {
238-
SvgTextUtil.processWhiteSpace(this, true);
253+
SvgTextUtil.processWhiteSpace(this, true, context);
239254
}
240255

241256
this.paragraph = new Paragraph();
@@ -311,10 +326,10 @@ void addTextChild(Text text, SvgDrawContext drawContext) {
311326
}
312327

313328
void performDrawing(SvgDrawContext context) {
314-
if (this.containsAbsolutePositionChange()) {
329+
if (this.containsAbsolutePositionChange(context)) {
315330
drawLastTextChunk(context);
316331
// TODO: DEVSIX-2507 support rotate and other attributes
317-
float[][] absolutePositions = this.getAbsolutePositionChanges();
332+
float[][] absolutePositions = this.getAbsolutePositionChanges(context);
318333
AffineTransform newTransform = getTextTransform(absolutePositions, context);
319334
startNewTextChunk(context, newTransform);
320335
}
@@ -383,7 +398,7 @@ void applyFillAndStrokeProperties(FillProperties fillProperties, StrokePropertie
383398
}
384399
}
385400

386-
private void resolveTextMove(SvgDrawContext context) {
401+
private void resolveRelativeTextMove(SvgDrawContext context) {
387402
if (this.attributesAndStyles != null) {
388403
String xRawValue = this.attributesAndStyles.get(SvgConstants.Attributes.DX);
389404
String yRawValue = this.attributesAndStyles.get(SvgConstants.Attributes.DY);
@@ -405,18 +420,39 @@ private void resolveTextMove(SvgDrawContext context) {
405420
}
406421
}
407422

408-
private void resolveTextPosition() {
423+
private void resolveAbsoluteTextPosition(SvgDrawContext context) {
409424
if (this.attributesAndStyles != null) {
410425
String xRawValue = this.attributesAndStyles.get(SvgConstants.Attributes.X);
411426
String yRawValue = this.attributesAndStyles.get(SvgConstants.Attributes.Y);
412427

413-
xPos = getPositionsFromString(xRawValue);
414-
yPos = getPositionsFromString(yRawValue);
428+
xPos = getPositionsFromString(xRawValue, context, true);
429+
yPos = getPositionsFromString(yRawValue, context, false);
415430

416431
posResolved = true;
417432
}
418433
}
419434

435+
private float[] getPositionsFromString(String rawValuesString, SvgDrawContext context, boolean isHorizontal) {
436+
float[] result = null;
437+
List<String> valuesList = SvgCssUtils.splitValueList(rawValuesString);
438+
if (!valuesList.isEmpty()) {
439+
result = new float[valuesList.size()];
440+
for (int i = 0; i < valuesList.size(); i++) {
441+
String value = valuesList.get(i);
442+
if (CssDimensionParsingUtils.determinePositionBetweenValueAndUnit(value) == 0) {
443+
throw new StyledXMLParserException(MessageFormatUtil.format(StyledXMLParserException.NAN, value));
444+
}
445+
if (isHorizontal) {
446+
result[i] = parseHorizontalLength(value, context);
447+
} else {
448+
result[i] = parseVerticalLength(value, context);
449+
}
450+
}
451+
}
452+
453+
return result;
454+
}
455+
420456
private static AffineTransform getTextTransform(float[][] absolutePositions, SvgDrawContext context) {
421457
AffineTransform tf = new AffineTransform();
422458
// If x is not specified, but y is, we need to correct for preceding text.
@@ -435,19 +471,6 @@ private static AffineTransform getTextTransform(float[][] absolutePositions, Svg
435471
return tf;
436472
}
437473

438-
private static float[] getPositionsFromString(String rawValuesString) {
439-
float[] result = null;
440-
List<String> valuesList = SvgCssUtils.splitValueList(rawValuesString);
441-
if (!valuesList.isEmpty()) {
442-
result = new float[valuesList.size()];
443-
for (int i = 0; i < valuesList.size(); i++) {
444-
result[i] = CssDimensionParsingUtils.parseAbsoluteLength(valuesList.get(i));
445-
}
446-
}
447-
448-
return result;
449-
}
450-
451474
/**
452475
* Adjust absolutely positioned text chunk (shift it to the start of view port, apply text anchor) and
453476
* merge it with the common text rectangle.
@@ -509,14 +532,16 @@ private void applyTextAnchor(String textAnchorValue, boolean isRtl) {
509532
}
510533

511534
private Point getStartPoint(SvgDrawContext context, Point basePoint) {
512-
double x = 0, y = 0;
513-
if (getAbsolutePositionChanges()[0] != null) {
514-
x = getAbsolutePositionChanges()[0][0];
535+
double x = 0;
536+
double y = 0;
537+
float[][] absolutePosition = getAbsolutePositionChanges(context);
538+
if (absolutePosition[0] != null) {
539+
x = absolutePosition[0][0];
515540
} else if (basePoint != null) {
516541
x = basePoint.getX();
517542
}
518-
if (getAbsolutePositionChanges()[1] != null) {
519-
y = getAbsolutePositionChanges()[1][0];
543+
if (absolutePosition[1] != null) {
544+
y = absolutePosition[1][0];
520545
} else if (basePoint != null) {
521546
y = basePoint.getY();
522547
}

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,31 @@ public static String trimTrailingWhitespace(String toTrim) {
111111
}
112112
}
113113

114-
115114
/**
116115
* Process the whitespace inside the Text Tree.
117116
* Whitespace is collapsed and new lines are handled
118117
* A leading element in each subtree is handled different: the preceding whitespace is trimmed instead of kept
119118
*
120119
* @param root root of the text-renderer subtree
121120
* @param isLeadingElement true if this element is a leading element(either the first child or the first element after an absolute position change)
121+
*
122+
* @deprecated use {@link #processWhiteSpace(TextSvgBranchRenderer, boolean, SvgDrawContext)} instead
122123
*/
124+
@Deprecated
123125
public static void processWhiteSpace(TextSvgBranchRenderer root, boolean isLeadingElement) {
126+
processWhiteSpace(root, isLeadingElement, new SvgDrawContext(null, null));
127+
}
128+
129+
/**
130+
* Process the whitespace inside the Text Tree.
131+
* Whitespace is collapsed and new lines are handled
132+
* A leading element in each subtree is handled different: the preceding whitespace is trimmed instead of kept
133+
*
134+
* @param root root of the text-renderer subtree
135+
* @param isLeadingElement true if this element is a leading element(either the first child or the first element after an absolute position change)
136+
* @param context the svg draw context
137+
*/
138+
public static void processWhiteSpace(TextSvgBranchRenderer root, boolean isLeadingElement, SvgDrawContext context) {
124139
// When svg is parsed by jsoup it leaves all whitespace in text element as is. Meaning that
125140
// tab/space indented xml files will retain their tabs and spaces.
126141
// The following regex replaces all whitespace with a single space.
@@ -144,9 +159,10 @@ public static void processWhiteSpace(TextSvgBranchRenderer root, boolean isLeadi
144159
// If child is leaf, process contents, if it is branch, call function again.
145160
if (child instanceof TextSvgBranchRenderer) {
146161
// Branch processing.
147-
processWhiteSpace((TextSvgBranchRenderer) child,
148-
child.containsAbsolutePositionChange() || isLeadingElement);
149-
((TextSvgBranchRenderer) child).markWhiteSpaceProcessed();
162+
TextSvgBranchRenderer childText = (TextSvgBranchRenderer) child;
163+
processWhiteSpace(childText,
164+
childText.containsAbsolutePositionChange(context) || isLeadingElement, context);
165+
childText.markWhiteSpaceProcessed();
150166
isLeadingElement = false;
151167
}
152168
if (child instanceof TextLeafSvgNodeRenderer) {

svg/src/test/java/com/itextpdf/svg/renderers/impl/TextSvgBranchRendererIntegrationTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ This file is part of the iText (R) project.
2727
import com.itextpdf.test.ITextTest;
2828

2929
import java.io.IOException;
30-
3130
import org.junit.jupiter.api.Assertions;
3231
import org.junit.jupiter.api.BeforeAll;
33-
import org.junit.jupiter.api.Test;
3432
import org.junit.jupiter.api.Tag;
33+
import org.junit.jupiter.api.Test;
3534

3635
@Tag("IntegrationTest")
3736
public class TextSvgBranchRendererIntegrationTest extends SvgIntegrationTest {
@@ -325,4 +324,9 @@ public void textWhiteSpacePreWrapTest() throws IOException, InterruptedException
325324
public void textWhiteSpacePreLineTest() throws IOException, InterruptedException {
326325
convertAndCompareSinglePage(SOURCE_FOLDER, DESTINATION_FOLDER, "textWhiteSpacePreLine");
327326
}
327+
328+
@Test
329+
public void textRelativeXYTest() throws IOException, InterruptedException {
330+
convertAndCompare(SOURCE_FOLDER, DESTINATION_FOLDER, "textRelativeXY");
331+
}
328332
}

svg/src/test/java/com/itextpdf/svg/utils/SvgTextUtilTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.svg.utils;
2424

2525
import com.itextpdf.svg.SvgConstants;
26+
import com.itextpdf.svg.renderers.SvgDrawContext;
2627
import com.itextpdf.svg.renderers.impl.TextLeafSvgNodeRenderer;
2728
import com.itextpdf.svg.renderers.impl.TextSvgBranchRenderer;
2829
import com.itextpdf.test.ExtendedITextTest;
30+
2931
import org.junit.jupiter.api.Assertions;
30-
import org.junit.jupiter.api.Test;
3132
import org.junit.jupiter.api.Tag;
33+
import org.junit.jupiter.api.Test;
3234

3335
@Tag("UnitTest")
3436
public class SvgTextUtilTest extends ExtendedITextTest {
@@ -216,7 +218,7 @@ public void processWhiteSpaceBreakLine() {
216218
root.addChild(textAfter);
217219

218220
//Run
219-
SvgTextUtil.processWhiteSpace(root, true);
221+
SvgTextUtil.processWhiteSpace(root, true, new SvgDrawContext(null, null));
220222
root.getChildren().get(0).getAttribute(SvgConstants.Attributes.TEXT_CONTENT);
221223
//Create result array
222224
String[] actual = new String[]{
@@ -260,7 +262,7 @@ public void processWhiteSpaceAbsPositionChange() {
260262
root.addChild(textAfter);
261263

262264
//Run
263-
SvgTextUtil.processWhiteSpace(root, true);
265+
SvgTextUtil.processWhiteSpace(root, true, new SvgDrawContext(null, null));
264266
root.getChildren().get(0).getAttribute(SvgConstants.Attributes.TEXT_CONTENT);
265267
//Create result array
266268
String[] actual = new String[]{
Lines changed: 36 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)