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)); + } +}