Skip to content

Commit 2d8edb7

Browse files
authored
Merge pull request #618 from danfickle/fix_615_endless_loop_char_breaking
Fix #615 endless loop char breaking caused by floats with top/bottom margins.
2 parents fcf4a1d + 370e0e3 commit 2d8edb7

File tree

13 files changed

+420
-195
lines changed

13 files changed

+420
-195
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## CHANGELOG
22

33
### head - 1.0.6-SNAPSHOT
4+
**IMPORTANT:** [#615](https://github.com/danfickle/openhtmltopdf/issues/615) This is a bug fix release for an endless loop issue when using break-word with floating elements with a top/bottom margin.
45
+ See commit log.
56

67

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ from ````/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/testcases/Testc
6868
## CHANGELOG
6969

7070
### head - 1.0.6-SNAPSHOT
71+
**IMPORTANT:** [#615](https://github.com/danfickle/openhtmltopdf/issues/615) This is a bug fix release for an endless loop issue when using break-word with floating elements with a top/bottom margin.
7172
+ See commit log.
7273

7374

openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/BlockFormattingContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public void clear(LayoutContext c, Box current) {
8484
getFloatManager().clear(c, this, current);
8585
}
8686

87+
@Override
8788
public String toString() {
8889
return "BlockFormattingContext: (" + _x + "," + _y + ")";
8990
}

openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java

Lines changed: 141 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -72,94 +72,160 @@ private static int getFirstLetterEnd(String text, int start) {
7272
return end;
7373
}
7474

75-
public static LineBreakResult breakText(LayoutContext c,
76-
LineBreakContext context, int avail,
77-
CalculatedStyle style, boolean tryToBreakAnywhere, int lineWidth) {
78-
75+
public enum BreakTextResult {
76+
/**
77+
* Has completely consumed the string.
78+
*/
79+
FINISHED,
80+
81+
/**
82+
* In char breaking mode, need a newline before continuing.
83+
* At least one character has been consumed.
84+
*/
85+
CONTINUE_CHAR_BREAKING_ON_NL,
86+
87+
/**
88+
* In word breaking mode, need a newline before continuing.
89+
* At least one word has been consumed.
90+
*/
91+
CONTINUE_WORD_BREAKING_ON_NL,
92+
93+
/**
94+
* Not a single char fitted, but we consumed one character anyway.
95+
*/
96+
CHAR_UNBREAKABLE_BUT_CONSUMED,
97+
98+
/**
99+
* Not a single word fitted, but we consumed a word anyway.
100+
* Only returned when word-wrap is not break-word.
101+
*/
102+
WORD_UNBREAKABLE_BUT_CONSUMED,
103+
104+
/**
105+
* DANGER: Last char did not fit and we are directing the
106+
* parent method to reconsume char on a newline.
107+
* Example, below floats.
108+
*/
109+
DANGER_RECONSUME_CHAR_ON_NL,
110+
111+
/**
112+
* DANGER: Last word did not fit and we are directing the
113+
* parent method to reconsume word on a newline.
114+
* Example, below floats.
115+
*/
116+
DANGER_RECONSUME_WORD_ON_NL;
117+
}
118+
119+
public static BreakTextResult breakText(
120+
LayoutContext c,
121+
LineBreakContext context,
122+
int avail,
123+
CalculatedStyle style,
124+
boolean tryToBreakAnywhere,
125+
int lineWidth,
126+
boolean forceOutput) {
127+
79128
FSFont font = style.getFSFont(c);
80129
IdentValue whitespace = style.getWhitespace();
81-
float letterSpacing = style.hasLetterSpacing() ?
130+
float letterSpacing = style.hasLetterSpacing() ?
82131
style.getFloatPropertyProportionalWidth(CSSName.LETTER_SPACING, 0, c) : 0f;
83132

84133
// ====== handle nowrap
85134
if (whitespace == IdentValue.NOWRAP) {
86135
int width = Breaker.getTextWidthWithLetterSpacing(c, font, context.getMaster(), letterSpacing);
87-
if (width <= avail) {
136+
if (width <= avail || forceOutput) {
88137
c.setLineBreakedBecauseOfNoWrap(false);
89138
context.setEnd(context.getLast());
90139
context.setWidth(width);
91-
return LineBreakResult.WORD_BREAKING_FINISHED;
140+
context.setNeedsNewLine(false);
141+
return BreakTextResult.FINISHED;
92142
} else if (!c.isLineBreakedBecauseOfNoWrap()) {
93143
c.setLineBreakedBecauseOfNoWrap(true);
94144
context.setEnd(context.getStart());
95145
context.setWidth(0);
96146
context.setNeedsNewLine(true);
97-
return LineBreakResult.WORD_BREAKING_NEED_NEW_LINE;
147+
context.setUnbreakable(true);
148+
return BreakTextResult.DANGER_RECONSUME_WORD_ON_NL;
98149
} else {
99150
c.setLineBreakedBecauseOfNoWrap(false);
100151
context.setEnd(context.getLast());
101152
context.setWidth(width);
102-
return LineBreakResult.WORD_BREAKING_FINISHED;
153+
context.setNeedsNewLine(false);
154+
return BreakTextResult.FINISHED;
103155
}
104156
}
105157

106-
//check if we should break on the next newline
158+
// Check if we should break on the next newline
107159
if (whitespace == IdentValue.PRE ||
108-
whitespace == IdentValue.PRE_WRAP ||
109-
whitespace == IdentValue.PRE_LINE) {
160+
whitespace == IdentValue.PRE_WRAP ||
161+
whitespace == IdentValue.PRE_LINE) {
110162
int n = context.getStartSubstring().indexOf(WhitespaceStripper.EOL);
111163
if (n > -1) {
112164
context.setEnd(context.getStart() + n + 1);
113165
context.setWidth(Breaker.getTextWidthWithLetterSpacing(c, font, context.getCalculatedSubstring(), letterSpacing));
114166
context.setNeedsNewLine(true);
115167
context.setEndsOnNL(true);
116168
} else if (whitespace == IdentValue.PRE) {
117-
context.setEnd(context.getLast());
169+
context.setEnd(context.getLast());
118170
context.setWidth(Breaker.getTextWidthWithLetterSpacing(c, font, context.getCalculatedSubstring(), letterSpacing));
171+
context.setNeedsNewLine(false);
119172
}
120173
}
121174

122-
//check if we may wrap
175+
// Check if we may wrap
123176
if (whitespace == IdentValue.PRE ||
124-
(context.isNeedsNewLine() && context.getWidth() <= avail)) {
177+
(context.isNeedsNewLine() && context.getWidth() <= avail)) {
125178
return context.isNeedsNewLine() ?
126-
LineBreakResult.WORD_BREAKING_NEED_NEW_LINE :
127-
LineBreakResult.WORD_BREAKING_FINISHED;
179+
BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL :
180+
BreakTextResult.FINISHED;
128181
}
129182

130183
context.setEndsOnNL(false);
131-
184+
132185
if (style.getWordWrap() != IdentValue.BREAK_WORD) {
133186
// Ordinary old word wrap which will overflow too long unbreakable words.
134-
return doBreakText(c, context, avail, style, tryToBreakAnywhere);
187+
return toBreakTextResult(
188+
doBreakText(c, context, avail, style, tryToBreakAnywhere));
135189
} else {
136190
int originalStart = context.getStart();
137191
int totalWidth = 0;
138192

139193
// The idea is we only break a word if it will not fit on a line by itself.
140-
194+
141195
LineBreakResult result;
196+
BreakTextResult breakResult;
197+
142198
LOOP:
143199
while (true) {
144200
int savedEnd = context.getEnd();
145201
result = doBreakText(c, context, avail, style, tryToBreakAnywhere);
146202

147203
switch (result) {
148-
case WORD_BREAKING_FINISHED:
204+
case WORD_BREAKING_FINISHED: /* Fallthru */
149205
case CHAR_BREAKING_FINISHED:
206+
totalWidth += context.getWidth();
207+
breakResult = BreakTextResult.FINISHED;
208+
break LOOP;
209+
150210
case CHAR_BREAKING_NEED_NEW_LINE:
151211
totalWidth += context.getWidth();
212+
breakResult = BreakTextResult.CONTINUE_CHAR_BREAKING_ON_NL;
152213
break LOOP;
153214

154215
case CHAR_BREAKING_UNBREAKABLE:
155-
if (totalWidth == 0 &&
156-
avail == lineWidth) {
216+
if ((totalWidth == 0 && avail == lineWidth) ||
217+
forceOutput) {
157218
// We are at the start of the line but could not fit a single character!
158219
totalWidth += context.getWidth();
220+
breakResult = BreakTextResult.CHAR_UNBREAKABLE_BUT_CONSUMED;
159221
break LOOP;
160222
} else {
161223
// We may be at the end of the line, so pick up at next line.
224+
// FIXME: This is very dangerous and has led to infinite
225+
// loops. Needs review.
162226
context.setEnd(savedEnd);
227+
context.setNeedsNewLine(true);
228+
breakResult = BreakTextResult.DANGER_RECONSUME_CHAR_ON_NL;
163229
break LOOP;
164230
}
165231

@@ -177,12 +243,14 @@ public static LineBreakResult breakText(LayoutContext c,
177243
} else {
178244
// Else, finish so it can be put on a new line.
179245
totalWidth += context.getWidth();
246+
breakResult = BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL;
180247
break LOOP;
181248
}
182249
}
183250
case WORD_BREAKING_UNBREAKABLE: {
184251
if (context.getWidth() >= lineWidth ||
185-
context.isFirstCharInLine()) {
252+
context.isFirstCharInLine() ||
253+
forceOutput) {
186254
// If the word is too long to fit on a line by itself or
187255
// if we are at the start of a line,
188256
// retry in character breaking mode.
@@ -194,28 +262,57 @@ public static LineBreakResult breakText(LayoutContext c,
194262
// FIXME: This is very dangerous and has led to infinite
195263
// loops. Needs review.
196264
context.setEnd(savedEnd);
265+
breakResult = BreakTextResult.DANGER_RECONSUME_WORD_ON_NL;
197266
break LOOP;
198267
}
199268
}
200269
}
201-
270+
202271
context.setStart(context.getEnd());
203272
avail -= context.getWidth();
204273
totalWidth += context.getWidth();
205274
}
206275

207276
context.setStart(originalStart);
208277
context.setWidth(totalWidth);
209-
278+
210279
// We need to know this for the next line.
211280
context.setFinishedInCharBreakingMode(tryToBreakAnywhere);
212-
return result;
281+
return breakResult;
213282
}
214283
}
215-
216-
private static LineBreakResult doBreakText(LayoutContext c,
217-
LineBreakContext context, int avail, CalculatedStyle style,
284+
285+
/**
286+
* Converts a LineBreakResult returned from doBreakText in
287+
* word-wrapping mode to a BreakTextResult.
288+
*
289+
* Throws a runtime exception if unexpected result found.
290+
*/
291+
private static BreakTextResult toBreakTextResult(LineBreakResult res) {
292+
switch (res) {
293+
case WORD_BREAKING_FINISHED:
294+
return BreakTextResult.FINISHED;
295+
case WORD_BREAKING_NEED_NEW_LINE:
296+
return BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL;
297+
case WORD_BREAKING_UNBREAKABLE:
298+
return BreakTextResult.WORD_UNBREAKABLE_BUT_CONSUMED;
299+
300+
case CHAR_BREAKING_FINISHED: // Fall-thru
301+
case CHAR_BREAKING_FOUND_WORD_BREAK: // Fall-thru
302+
case CHAR_BREAKING_NEED_NEW_LINE: // Fall-thru
303+
case CHAR_BREAKING_UNBREAKABLE: // Fall-thru
304+
default:
305+
throw new RuntimeException("PROGRAMMER ERROR: Unexpected LineBreakResult from word wrap");
306+
}
307+
}
308+
309+
private static LineBreakResult doBreakText(
310+
LayoutContext c,
311+
LineBreakContext context,
312+
int avail,
313+
CalculatedStyle style,
218314
boolean tryToBreakAnywhere) {
315+
219316
if (!tryToBreakAnywhere) {
220317
return doBreakText(c, context, avail, style, STANDARD_LINE_BREAKER);
221318
} else {
@@ -227,15 +324,15 @@ private static LineBreakResult doBreakText(LayoutContext c,
227324

228325
ToIntFunction<String> measurer = (str) ->
229326
c.getTextRenderer().getWidth(c.getFontContext(), font, str);
230-
327+
231328
String currentString = context.getStartSubstring();
232329
FSTextBreaker lineIterator = STANDARD_LINE_BREAKER.getBreaker(currentString, c.getSharedContext());
233-
FSTextBreaker charIterator = STANDARD_CHARACTER_BREAKER.getBreaker(currentString, c.getSharedContext());
234-
330+
FSTextBreaker charIterator = STANDARD_CHARACTER_BREAKER.getBreaker(currentString, c.getSharedContext());
331+
235332
return doBreakCharacters(currentString, lineIterator, charIterator, context, avail, letterSpacing, measurer);
236333
}
237334
}
238-
335+
239336
/**
240337
* Breaks at most one word (until the next word break) going character by character to see
241338
* what will fit in.
@@ -303,11 +400,12 @@ static LineBreakResult doBreakCharacters(
303400
graphicsLength > 0) {
304401
// Exact fit..
305402
boolean needNewLine = currentString.length() > left;
306-
403+
307404
context.setNeedsNewLine(needNewLine);
308405
context.setEnd(left + context.getStart());
309406
context.setWidth(graphicsLength);
310-
407+
context.setUnbreakable(false);
408+
311409
if (left >= currentString.length()) {
312410
return LineBreakResult.CHAR_BREAKING_FINISHED;
313411
} else if (left >= nextWordBreak) {
@@ -340,6 +438,7 @@ static LineBreakResult doBreakCharacters(
340438
context.setWidth(graphicsLength);
341439
context.setEnd(nextCharBreak + context.getStart());
342440
context.setEndsOnWordBreak(nextCharBreak == nextWordBreak);
441+
context.setUnbreakable(false);
343442

344443
if (nextCharBreak >= currentString.length()) {
345444
return LineBreakResult.CHAR_BREAKING_FINISHED;
@@ -358,8 +457,10 @@ static LineBreakResult doBreakCharacters(
358457
context.setWidth(lastGoodGraphicsLength);
359458
context.setEnd(lastGoodWrap + context.getStart());
360459
context.setEndsOnWordBreak(lastGoodWrap == nextWordBreak);
460+
context.setUnbreakable(false);
361461

362462
if (lastGoodWrap >= currentString.length()) {
463+
context.setNeedsNewLine(false);
363464
return LineBreakResult.CHAR_BREAKING_FINISHED;
364465
} else if (lastGoodWrap >= nextWordBreak) {
365466
return LineBreakResult.CHAR_BREAKING_FOUND_WORD_BREAK;
@@ -376,13 +477,15 @@ static LineBreakResult doBreakCharacters(
376477
context.setEnd(end + context.getStart());
377478
context.setEndsOnWordBreak(end == nextWordBreak);
378479
context.setWidth(splitWidth);
379-
480+
context.setNeedsNewLine(end < currentString.length());
481+
380482
return LineBreakResult.CHAR_BREAKING_UNBREAKABLE;
381483
} else {
382484
// Empty string.
383485
context.setEnd(context.getStart());
384486
context.setWidth(0);
385487
context.setNeedsNewLine(false);
488+
context.setUnbreakable(false);
386489

387490
return LineBreakResult.CHAR_BREAKING_FINISHED;
388491
}
@@ -498,6 +601,7 @@ static LineBreakResult doBreakTextWords(
498601
if (current.graphicsLength <= avail) {
499602
context.setWidth(current.graphicsLength);
500603
context.setEnd(context.getMaster().length());
604+
context.setNeedsNewLine(false);
501605
// It all fit!
502606
return LineBreakResult.WORD_BREAKING_FINISHED;
503607
}

0 commit comments

Comments
 (0)