diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java
index 79d20e02c..f1f9f2fe1 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java
@@ -388,7 +388,7 @@ void copyTo(AppBreakOpportunity other) {
other.isSoftHyphenBreak = isSoftHyphenBreak;
}
}
-
+
public static LineBreakResult doBreakText(
LayoutContext c,
LineBreakContext context,
@@ -402,8 +402,22 @@ public static LineBreakResult doBreakText(
? style.getFloatPropertyProportionalWidth(CSSName.LETTER_SPACING, 0, c)
: 0f;
+ ToIntFunction measurer = (str) ->
+ c.getTextRenderer().getWidth(c.getFontContext(), font, str);
+
String currentString = context.getStartSubstring();
- FSTextBreaker iterator = lineBreaker.getBreaker(currentString, c.getSharedContext());
+ FSTextBreaker lineIterator = lineBreaker.getBreaker(currentString, c.getSharedContext());
+
+ return doBreakTextWords(currentString, context, avail, lineIterator, letterSpacing, measurer);
+ }
+
+ static LineBreakResult doBreakTextWords(
+ String currentString,
+ LineBreakContext context,
+ int avail,
+ FSTextBreaker iterator,
+ float letterSpacing,
+ ToIntFunction measurer) {
int lastWrap = 0;
@@ -423,13 +437,12 @@ public static LineBreakResult doBreakText(
String subString = currentString.substring(current.left, current.right);
float extraSpacing = (current.right - current.left) * letterSpacing;
- int normalSplitWidth = (int) (c.getTextRenderer().getWidth(
- c.getFontContext(), font, subString) + extraSpacing);
-
+ int normalSplitWidth = (int) (measurer.applyAsInt(subString) + extraSpacing);
+
if (currentString.charAt(current.right - 1) == SOFT_HYPHEN) {
current.isSoftHyphenBreak = true;
- int withTrailingHyphenSplitWidth = (int) (c.getTextRenderer().getWidth(
- c.getFontContext(), font, subString + '-') +
+ int withTrailingHyphenSplitWidth = (int)
+ (measurer.applyAsInt(subString + '-') +
extraSpacing + letterSpacing);
current.withHyphenGraphicsLength = current.graphicsLength + withTrailingHyphenSplitWidth;
@@ -458,8 +471,8 @@ public static LineBreakResult doBreakText(
current.copyTo(prev);
current.right = currentString.length();
float extraSpacing = (current.right - current.left) * letterSpacing;
- int splitWidth = (int) (c.getTextRenderer().getWidth(
- c.getFontContext(), font, currentString.substring(current.left)) + extraSpacing);
+ int splitWidth = (int) (measurer.applyAsInt(
+ currentString.substring(current.left)) + extraSpacing);
current.graphicsLength += splitWidth;
nextUnfittableSplitWidth = splitWidth;
}
@@ -500,8 +513,7 @@ public static LineBreakResult doBreakText(
} else if (current.left == currentString.length()) {
String text = context.getCalculatedSubstring();
float extraSpacing = text.length() * letterSpacing;
- context.setWidth((int) (c.getTextRenderer().getWidth(
- c.getFontContext(), font, text) + extraSpacing));
+ context.setWidth((int) (measurer.applyAsInt(text) + extraSpacing));
} else {
context.setWidth(current.graphicsLength);
}
diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/LineBreakContext.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/LineBreakContext.java
index e9fde11d1..d04434894 100644
--- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/LineBreakContext.java
+++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/LineBreakContext.java
@@ -153,7 +153,7 @@ public boolean isEndsOnSoftHyphen() {
}
public void setEndsOnSoftHyphen(boolean b) {
- this._endsOnSoftHyphen = true;
+ this._endsOnSoftHyphen = b;
}
/**
diff --git a/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTest.java b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTest.java
index cb7bccf42..447a39713 100644
--- a/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTest.java
+++ b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTest.java
@@ -1,7 +1,5 @@
package com.openhtmltopdf.layout;
-import java.util.function.ToIntFunction;
-
import org.junit.Test;
import static org.hamcrest.core.IsEqual.equalTo;
@@ -9,64 +7,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
-import com.openhtmltopdf.extend.FSTextBreaker;
+import static com.openhtmltopdf.layout.BreakerTestSupport.*;
public class BreakerTest {
- private static class SimpleCharBreaker implements FSTextBreaker {
- private String text;
- private int pos;
-
- @Override
- public int next() {
- return pos > text.length() ? -1 : pos++;
- }
-
- @Override
- public void setText(String newText) {
- this.text = newText;
- this.pos = 0;
- }
- }
-
- private static class SimpleLineBreaker implements FSTextBreaker {
- private String text;
- private int position;
-
- @Override
- public int next() {
- int ret = text.indexOf(' ', this.position);
- this.position = ret < 0 ? -1 : ret + 1;
- return ret;
- }
-
- @Override
- public void setText(String newText) {
- this.text = newText;
- this.position = 0;
- }
- }
-
- private FSTextBreaker createLine(String line) {
- SimpleLineBreaker breaker = new SimpleLineBreaker();
- breaker.setText(line);
- return breaker;
- }
-
- private FSTextBreaker createChar(String line) {
- FSTextBreaker breaker = new SimpleCharBreaker();
- breaker.setText(line);
- return breaker;
- }
-
- private final ToIntFunction MEASURER = (str) -> str.length();
- private final ToIntFunction MEASURER3 = (str) -> str.length() * 3;
-
- private LineBreakContext createContext(String str) {
- LineBreakContext ctx = new LineBreakContext();
- ctx.setMaster(str);
- return ctx;
- }
-
@Test
public void testCharacterBreakerSingleChar() {
String whole = "A";
@@ -108,8 +51,8 @@ public void testCharacterBreakerUntilWord() {
assertFalse(context.isUnbreakable());
assertFalse(context.isNeedsNewLine());
- assertThat(context.getWidth(), equalTo(4));
- assertThat(context.getEnd(), equalTo(4));
+ assertThat(context.getWidth(), equalTo(5));
+ assertThat(context.getEnd(), equalTo(5));
}
@Test
diff --git a/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTestSupport.java b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTestSupport.java
new file mode 100644
index 000000000..cfe3222d0
--- /dev/null
+++ b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/BreakerTestSupport.java
@@ -0,0 +1,122 @@
+package com.openhtmltopdf.layout;
+
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.function.ToIntFunction;
+
+import com.openhtmltopdf.extend.FSTextBreaker;
+
+public class BreakerTestSupport {
+ enum ContextIs {
+ UNBREAKABLE,
+ NEEDS_NEW_LINE,
+ FINISHED_IN_CHAR_BREAKING_MODE,
+ FINISHED,
+ ENDS_ON_NL,
+ /**
+ * This should really be called ENDS_ON_VISIBLE_SOFT_HYPHEN.
+ */
+ ENDS_ON_SOFT_HYPHEN,
+ ENDS_ON_WORD_BREAK
+ }
+
+ static void assertContextIs(LineBreakContext context, ContextIs... trues) {
+ EnumSet truthy = trues.length > 0 ?
+ EnumSet.copyOf(Arrays.asList(trues)) :
+ EnumSet.noneOf(ContextIs.class);
+ boolean desired;
+
+ desired = truthy.contains(ContextIs.UNBREAKABLE);
+ assertThat(context.isUnbreakable(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.NEEDS_NEW_LINE);
+ assertThat(context.isNeedsNewLine(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.FINISHED_IN_CHAR_BREAKING_MODE);
+ assertThat(context.isFinishedInCharBreakingMode(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.FINISHED);
+ assertThat(context.isFinished(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.ENDS_ON_NL);
+ assertThat(context.isEndsOnNL(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.ENDS_ON_SOFT_HYPHEN);
+ assertThat(context.isEndsOnSoftHyphen(), equalTo(desired));
+
+ desired = truthy.contains(ContextIs.ENDS_ON_WORD_BREAK);
+ assertThat(context.isEndsOnWordBreak(), equalTo(desired));
+ }
+
+ private static class SimpleCharBreaker implements FSTextBreaker {
+ private String text;
+ private int pos;
+
+ @Override
+ public int next() {
+ return pos > text.length() ? -1 : pos++;
+ }
+
+ @Override
+ public void setText(String newText) {
+ this.text = newText;
+ this.pos = 0;
+ }
+ }
+
+ private static class SimpleLineBreaker implements FSTextBreaker {
+ private String text;
+ private int position;
+
+ @Override
+ public int next() {
+ if (this.position == -1) {
+ return -1;
+ }
+
+ int ret = text.indexOf(' ', this.position);
+ int softHyphen = text.indexOf('\u00ad', this.position);
+
+ if (softHyphen != -1 && (softHyphen < ret || ret == -1)) {
+ ret = softHyphen;
+ }
+
+ this.position = ret < 0 ? -1 : ret + 1;
+ return this.position;
+ }
+
+ @Override
+ public void setText(String newText) {
+ this.text = newText;
+ this.position = 0;
+ }
+ }
+
+ static FSTextBreaker createLine(String line) {
+ SimpleLineBreaker breaker = new SimpleLineBreaker();
+ breaker.setText(line);
+ return breaker;
+ }
+
+ static FSTextBreaker createChar(String line) {
+ FSTextBreaker breaker = new SimpleCharBreaker();
+ breaker.setText(line);
+ return breaker;
+ }
+
+ static LineBreakContext createContext(String str) {
+ LineBreakContext ctx = new LineBreakContext();
+ ctx.setMaster(str);
+ return ctx;
+ }
+
+ static final ToIntFunction MEASURER = (str) -> str.length();
+ static final ToIntFunction MEASURER3 = (str) -> str.length() * 3;
+ static final ToIntFunction MEASURER_WITH_ZERO_WIDTH_SOFT_HYPHEN = (str) -> {
+ long softHyphenCount = str.chars().filter(ch -> ch == Breaker.SOFT_HYPHEN).count();
+ return (int) (str.length() - softHyphenCount);
+ };
+}
diff --git a/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/WordBreakerTest.java b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/WordBreakerTest.java
new file mode 100644
index 000000000..6a8a187ce
--- /dev/null
+++ b/openhtmltopdf-core/src/test/java/com/openhtmltopdf/layout/WordBreakerTest.java
@@ -0,0 +1,179 @@
+package com.openhtmltopdf.layout;
+
+import static com.openhtmltopdf.layout.BreakerTestSupport.*;
+import static com.openhtmltopdf.layout.BreakerTestSupport.createContext;
+import static com.openhtmltopdf.layout.BreakerTestSupport.createLine;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+import com.openhtmltopdf.layout.LineBreakContext.LineBreakResult;
+
+import static com.openhtmltopdf.layout.BreakerTestSupport.ContextIs.*;
+import static com.openhtmltopdf.layout.BreakerTestSupport.assertContextIs;
+
+public class WordBreakerTest {
+ @Test
+ public void testSingleCharFits() {
+ String whole = "A";
+ int avail = 1;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(1));
+ assertThat(context.getEnd(), equalTo(1));
+ }
+
+ @Test
+ public void testSingleCharDoesNotFitWithWidthZero() {
+ String whole = "A";
+ int avail = 0;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_UNBREAKABLE));
+ assertContextIs(context, UNBREAKABLE, FINISHED, NEEDS_NEW_LINE);
+
+ assertThat(context.getWidth(), equalTo(1));
+ assertThat(context.getEnd(), equalTo(1));
+ }
+
+ @Test
+ public void testMultiWordDoesNotFitWithWidthZero() {
+ String whole = "A b c";
+ int avail = 0;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_UNBREAKABLE));
+ assertContextIs(context, UNBREAKABLE, NEEDS_NEW_LINE);
+
+ assertThat(context.getWidth(), equalTo(2));
+ assertThat(context.getEnd(), equalTo(2));
+ }
+
+ @Test
+ public void testSingleCharFitsWithLetterSpacing() {
+ String whole = "A";
+ int avail = 2;
+ float letterSpacing = 1;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(2));
+ assertThat(context.getEnd(), equalTo(1));
+ }
+
+ @Test
+ public void testMultiCharFitsWithLetterSpacing() {
+ String whole = "Abc";
+ int avail = 6;
+ float letterSpacing = 1;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(6));
+ assertThat(context.getEnd(), equalTo(3));
+ }
+
+ @Test
+ public void testMultiWordFitsWithLetterSpacing() {
+ String whole = "Abc def";
+ int avail = 14;
+ float letterSpacing = 1;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(14));
+ assertThat(context.getEnd(), equalTo(7));
+ }
+
+ @Test
+ public void testSingleSoftHyphenWithWidthFits() {
+ String whole = "\u00ad";
+ int avail = 2;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing, MEASURER);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(1));
+ assertThat(context.getEnd(), equalTo(1));
+ }
+
+ @Test
+ public void testSingleSoftHyphenWithOutWidthFits() {
+ String whole = "" + Breaker.SOFT_HYPHEN;
+ int avail = 1;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing,
+ MEASURER_WITH_ZERO_WIDTH_SOFT_HYPHEN);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(0));
+ assertThat(context.getEnd(), equalTo(1));
+ }
+
+ @Test
+ public void testTrailingSoftHyphenWithOutWidthFits() {
+ String whole = "abc\u00ad";
+ int avail = 4;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing,
+ MEASURER_WITH_ZERO_WIDTH_SOFT_HYPHEN);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_FINISHED));
+ assertContextIs(context, FINISHED);
+
+ assertThat(context.getWidth(), equalTo(3));
+ assertThat(context.getEnd(), equalTo(4));
+ }
+
+ @Test
+ public void testMiddleSoftHyphenWithOutWidthFits() {
+ String whole = "abc" + Breaker.SOFT_HYPHEN + "def";
+ int avail = 4;
+ float letterSpacing = 0;
+ LineBreakContext context = createContext(whole);
+
+ LineBreakResult res = Breaker.doBreakTextWords(whole, context, avail, createLine(whole), letterSpacing,
+ MEASURER_WITH_ZERO_WIDTH_SOFT_HYPHEN);
+
+ assertThat(res, equalTo(LineBreakResult.WORD_BREAKING_NEED_NEW_LINE));
+ assertContextIs(context, NEEDS_NEW_LINE, ENDS_ON_SOFT_HYPHEN);
+
+ assertThat(context.getWidth(), equalTo(4));
+ assertThat(context.getEnd(), equalTo(4));
+ }
+}