Skip to content

Commit

Permalink
#615 Sanity checks and fomatting
Browse files Browse the repository at this point in the history
Trying to make breaker more robust again.
  • Loading branch information
danfickle committed Dec 10, 2020
1 parent 6d9fac9 commit d7fdf63
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 34 deletions.
144 changes: 116 additions & 28 deletions openhtmltopdf-core/src/main/java/com/openhtmltopdf/layout/Breaker.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,61 @@ private static int getFirstLetterEnd(String text, int start) {
return end;
}

public static LineBreakResult breakText(LayoutContext c,
LineBreakContext context, int avail,
CalculatedStyle style, boolean tryToBreakAnywhere, int lineWidth) {

public enum BreakTextResult {
/**
* Has completely consumed the string.
*/
FINISHED,

/**
* In char breaking mode, need a newline before continuing.
* At least one character has been consumed.
*/
CONTINUE_CHAR_BREAKING_ON_NL,

/**
* In word breaking mode, need a newline before continuing.
* At least one word has been consumed.
*/
CONTINUE_WORD_BREAKING_ON_NL,

/**
* Not a single char fitted, but we consumed one character anyway.
*/
CHAR_UNBREAKABLE_BUT_CONSUMED,

/**
* Not a single word fitted, but we consumed a word anyway.
* Only returned when word-wrap is not break-word.
*/
WORD_UNBREAKABLE_BUT_CONSUMED,

/**
* DANGER: Last char did not fit and we are directing the
* parent method to reconsume char on a newline.
* Example, below floats.
*/
DANGER_RECONSUME_CHAR_ON_NL,

/**
* DANGER: Last word did not fit and we are directing the
* parent method to reconsume word on a newline.
* Example, below floats.
*/
DANGER_RECONSUME_WORD_ON_NL;
}

public static BreakTextResult breakText(
LayoutContext c,
LineBreakContext context,
int avail,
CalculatedStyle style,
boolean tryToBreakAnywhere,
int lineWidth) {

FSFont font = style.getFSFont(c);
IdentValue whitespace = style.getWhitespace();
float letterSpacing = style.hasLetterSpacing() ?
float letterSpacing = style.hasLetterSpacing() ?
style.getFloatPropertyProportionalWidth(CSSName.LETTER_SPACING, 0, c) : 0f;

// ====== handle nowrap
Expand All @@ -88,80 +136,90 @@ public static LineBreakResult breakText(LayoutContext c,
c.setLineBreakedBecauseOfNoWrap(false);
context.setEnd(context.getLast());
context.setWidth(width);
return LineBreakResult.WORD_BREAKING_FINISHED;
return BreakTextResult.FINISHED;
} else if (!c.isLineBreakedBecauseOfNoWrap()) {
c.setLineBreakedBecauseOfNoWrap(true);
context.setEnd(context.getStart());
context.setWidth(0);
context.setNeedsNewLine(true);
return LineBreakResult.WORD_BREAKING_NEED_NEW_LINE;
return BreakTextResult.DANGER_RECONSUME_WORD_ON_NL;
} else {
c.setLineBreakedBecauseOfNoWrap(false);
context.setEnd(context.getLast());
context.setWidth(width);
return LineBreakResult.WORD_BREAKING_FINISHED;
return BreakTextResult.FINISHED;
}
}

//check if we should break on the next newline
// Check if we should break on the next newline
if (whitespace == IdentValue.PRE ||
whitespace == IdentValue.PRE_WRAP ||
whitespace == IdentValue.PRE_LINE) {
whitespace == IdentValue.PRE_WRAP ||
whitespace == IdentValue.PRE_LINE) {
int n = context.getStartSubstring().indexOf(WhitespaceStripper.EOL);
if (n > -1) {
context.setEnd(context.getStart() + n + 1);
context.setWidth(Breaker.getTextWidthWithLetterSpacing(c, font, context.getCalculatedSubstring(), letterSpacing));
context.setNeedsNewLine(true);
context.setEndsOnNL(true);
} else if (whitespace == IdentValue.PRE) {
context.setEnd(context.getLast());
context.setEnd(context.getLast());
context.setWidth(Breaker.getTextWidthWithLetterSpacing(c, font, context.getCalculatedSubstring(), letterSpacing));
}
}

//check if we may wrap
// Check if we may wrap
if (whitespace == IdentValue.PRE ||
(context.isNeedsNewLine() && context.getWidth() <= avail)) {
(context.isNeedsNewLine() && context.getWidth() <= avail)) {
return context.isNeedsNewLine() ?
LineBreakResult.WORD_BREAKING_NEED_NEW_LINE :
LineBreakResult.WORD_BREAKING_FINISHED;
BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL :
BreakTextResult.FINISHED;
}

context.setEndsOnNL(false);

if (style.getWordWrap() != IdentValue.BREAK_WORD) {
// Ordinary old word wrap which will overflow too long unbreakable words.
return doBreakText(c, context, avail, style, tryToBreakAnywhere);
return toBreakTextResult(
doBreakText(c, context, avail, style, tryToBreakAnywhere));
} else {
int originalStart = context.getStart();
int totalWidth = 0;

// The idea is we only break a word if it will not fit on a line by itself.

LineBreakResult result;
BreakTextResult breakResult;

LOOP:
while (true) {
int savedEnd = context.getEnd();
result = doBreakText(c, context, avail, style, tryToBreakAnywhere);

switch (result) {
case WORD_BREAKING_FINISHED:
case WORD_BREAKING_FINISHED: /* Fallthru */
case CHAR_BREAKING_FINISHED:
totalWidth += context.getWidth();
breakResult = BreakTextResult.FINISHED;
break LOOP;

case CHAR_BREAKING_NEED_NEW_LINE:
totalWidth += context.getWidth();
breakResult = BreakTextResult.CONTINUE_CHAR_BREAKING_ON_NL;
break LOOP;

case CHAR_BREAKING_UNBREAKABLE:
if (totalWidth == 0 &&
avail == lineWidth) {
// We are at the start of the line but could not fit a single character!
totalWidth += context.getWidth();
breakResult = BreakTextResult.CHAR_UNBREAKABLE_BUT_CONSUMED;
break LOOP;
} else {
// We may be at the end of the line, so pick up at next line.
// FIXME: This is very dangerous and has led to infinite
// loops. Needs review.
context.setEnd(savedEnd);
breakResult = BreakTextResult.DANGER_RECONSUME_CHAR_ON_NL;
break LOOP;
}

Expand All @@ -179,6 +237,7 @@ public static LineBreakResult breakText(LayoutContext c,
} else {
// Else, finish so it can be put on a new line.
totalWidth += context.getWidth();
breakResult = BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL;
break LOOP;
}
}
Expand All @@ -196,28 +255,57 @@ public static LineBreakResult breakText(LayoutContext c,
// FIXME: This is very dangerous and has led to infinite
// loops. Needs review.
context.setEnd(savedEnd);
breakResult = BreakTextResult.DANGER_RECONSUME_WORD_ON_NL;
break LOOP;
}
}
}

context.setStart(context.getEnd());
avail -= context.getWidth();
totalWidth += context.getWidth();
}

context.setStart(originalStart);
context.setWidth(totalWidth);

// We need to know this for the next line.
context.setFinishedInCharBreakingMode(tryToBreakAnywhere);
return result;
return breakResult;
}
}

private static LineBreakResult doBreakText(LayoutContext c,
LineBreakContext context, int avail, CalculatedStyle style,

/**
* Converts a LineBreakResult returned from doBreakText in
* word-wrapping mode to a BreakTextResult.
*
* Throws a runtime exception if unexpected result found.
*/
private static BreakTextResult toBreakTextResult(LineBreakResult res) {
switch (res) {
case WORD_BREAKING_FINISHED:
return BreakTextResult.FINISHED;
case WORD_BREAKING_NEED_NEW_LINE:
return BreakTextResult.CONTINUE_WORD_BREAKING_ON_NL;
case WORD_BREAKING_UNBREAKABLE:
return BreakTextResult.WORD_UNBREAKABLE_BUT_CONSUMED;

case CHAR_BREAKING_FINISHED: // Fall-thru
case CHAR_BREAKING_FOUND_WORD_BREAK: // Fall-thru
case CHAR_BREAKING_NEED_NEW_LINE: // Fall-thru
case CHAR_BREAKING_UNBREAKABLE: // Fall-thru
default:
throw new RuntimeException(); // TODO: Log
}
}

private static LineBreakResult doBreakText(
LayoutContext c,
LineBreakContext context,
int avail,
CalculatedStyle style,
boolean tryToBreakAnywhere) {

if (!tryToBreakAnywhere) {
return doBreakText(c, context, avail, style, STANDARD_LINE_BREAKER);
} else {
Expand All @@ -229,11 +317,11 @@ private static LineBreakResult doBreakText(LayoutContext c,

ToIntFunction<String> measurer = (str) ->
c.getTextRenderer().getWidth(c.getFontContext(), font, str);

String currentString = context.getStartSubstring();
FSTextBreaker lineIterator = STANDARD_LINE_BREAKER.getBreaker(currentString, c.getSharedContext());
FSTextBreaker charIterator = STANDARD_CHARACTER_BREAKER.getBreaker(currentString, c.getSharedContext());

return doBreakCharacters(currentString, lineIterator, charIterator, context, avail, letterSpacing, measurer);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.openhtmltopdf.css.style.CssContext;
import com.openhtmltopdf.css.style.derived.BorderPropertySet;
import com.openhtmltopdf.css.style.derived.RectPropertySet;
import com.openhtmltopdf.layout.Breaker.BreakTextResult;
import com.openhtmltopdf.render.AnonymousBlockBox;
import com.openhtmltopdf.render.BlockBox;
import com.openhtmltopdf.render.Box;
Expand Down Expand Up @@ -1100,29 +1101,97 @@ private static void finishPendingInlineLayers(LayoutContext c, List<Layer> layer
}
}

private static InlineText layoutText(LayoutContext c, CalculatedStyle style, int remainingWidth,
LineBreakContext lbContext, boolean needFirstLetter,
byte textDirection, boolean tryToBreakAnywhere, int lineWidth) {
private static InlineText layoutText(
LayoutContext c,
CalculatedStyle style,
int remainingWidth,
LineBreakContext lbContext,
boolean needFirstLetter,
byte textDirection,
boolean tryToBreakAnywhere,
int lineWidth) {

InlineText result = new InlineText();
String masterText = lbContext.getMaster();

if (needFirstLetter) {
masterText = TextUtil.transformFirstLetterText(masterText, style);
lbContext.setMaster(masterText);
Breaker.breakFirstLetter(c, lbContext, remainingWidth, style);
} else {
Breaker.breakText(c, lbContext, remainingWidth, style, tryToBreakAnywhere, lineWidth);
BreakTextResult breakResult =
Breaker.breakText(c, lbContext, remainingWidth, style, tryToBreakAnywhere, lineWidth);
checkBreakResult(breakResult, lbContext, tryToBreakAnywhere);
}

result.setMasterText(masterText);
result.setSubstring(lbContext.getStart(), lbContext.getEnd());
result.setWidth(lbContext.getWidth());
result.setTextDirection(textDirection);
result.setEndsOnSoftHyphen(lbContext.isEndsOnSoftHyphen());

return result;
}

private static void checkBreakResult(
BreakTextResult breakResult,
LineBreakContext lbContext,
boolean tryToBreakAnywhere) {

switch (breakResult) {
case CONTINUE_CHAR_BREAKING_ON_NL:
if (lbContext.getStart() >= lbContext.getEnd() ||
!lbContext.isNeedsNewLine()) {
fail(breakResult, lbContext);
}
break;
case CONTINUE_WORD_BREAKING_ON_NL:
if (lbContext.getStart() >= lbContext.getEnd() ||
lbContext.isUnbreakable() ||
!lbContext.isNeedsNewLine()) {
fail(breakResult, lbContext);
}
break;
case DANGER_RECONSUME_CHAR_ON_NL:
if (lbContext.getStart() > lbContext.getEnd()) {
fail(breakResult, lbContext);
}
break;
case DANGER_RECONSUME_WORD_ON_NL:
if (lbContext.getStart() > lbContext.getEnd()) {
fail(breakResult, lbContext);
}
break;
case FINISHED:
if (!lbContext.isFinished() ||
lbContext.getStart() >= lbContext.getEnd() ||
lbContext.isUnbreakable() ||
lbContext.isNeedsNewLine()) {
fail(breakResult, lbContext);
}
break;
case WORD_UNBREAKABLE_BUT_CONSUMED:
if (!lbContext.isUnbreakable() ||
lbContext.getStart() >= lbContext.getEnd()) {
fail(breakResult, lbContext);
}
break;
case CHAR_UNBREAKABLE_BUT_CONSUMED:
if (!lbContext.isUnbreakable() ||
lbContext.getStart() >= lbContext.getEnd()) {
fail(breakResult, lbContext);
}
break;
}
}

private static void fail(
BreakTextResult breakResult,
LineBreakContext lbContext) {
// TODO: Log
throw new RuntimeException();
}

private static int processOutOfFlowContent(
LayoutContext c, LineBox current, BlockBox block,
int available, List<FloatLayoutResult> pendingFloats) {
Expand Down

0 comments on commit d7fdf63

Please sign in to comment.