Skip to content

Commit

Permalink
#364 Fix for double-adding footnotes when satisfying widows/orphans.
Browse files Browse the repository at this point in the history
Also box spliterator for descendants.
  • Loading branch information
danfickle committed Jun 2, 2021
1 parent 46b796d commit 5372cb5
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,15 @@ public void reset(LayoutContext c) {
if (_markerData != null) {
_markerData.restorePreviousReferenceLine(this);
}

if (hasFootnotes()) {
// Reset usually happens when satisfying widows and orphans.
// Reset means we and our descendants are about to be layed out again
// so we have to remove footnotes as they will be added again, possible on
// a new page.
c.getRootLayer().getFirstPage(c, this).removeFootnoteBodies(c, getReferencedFootnoteBodies());
}

super.reset(c);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package com.openhtmltopdf.util;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;

import com.openhtmltopdf.render.BlockBox;
import com.openhtmltopdf.render.Box;
import com.openhtmltopdf.render.InlineLayoutBox;

/**
* A spliterator that can be used to create a stream of descendant boxes in
* breadth first order.
* <br><br>
* NOTE: This is likely slower than simply creating a box list recursively for small
* number of descendants but gets relatively faster (and uses less memory)
* as the list of descendants increases in size.
*/
public class DescendantBoxSpliterator implements Spliterator<Box> {
// A queue of boxes that we have to process their children.
private final Deque<Box> unprocessed = new ArrayDeque<>();

private State curState = State.BOX_CHILDREN;
private int childIndex = 0;

private enum State {
BOX_CHILDREN,
BLOCK_INLINE_CONTENT,
ILB_INLINE_CHILDREN,
DONE;
}

public DescendantBoxSpliterator(Box parent) {
unprocessed.add(parent);
}

private boolean hasChildren(Box bx) {
return
bx.getChildCount() > 0 ||
(bx instanceof BlockBox && !((BlockBox) bx).getInlineContent().isEmpty()) ||
(bx instanceof InlineLayoutBox && ((InlineLayoutBox) bx).getInlineChildCount() > 0);
}

private void add(Box bx) {
unprocessed.add(bx);
}

private void next() {
setState(State.BOX_CHILDREN);
unprocessed.remove();
}

private boolean hasNext() {
return !unprocessed.isEmpty();
}

private Box current() {
return unprocessed.peek();
}

private void setState(State newState) {
curState = newState;
childIndex = 0;
}

private Box getNext() {
Box box = current();

if (box == null) {
return null;
}

switch (curState) {
case BOX_CHILDREN: {
if (childIndex < box.getChildCount()) {
Box next = box.getChild(childIndex);
childIndex++;

if (hasChildren(next)) {
add(next);
}

return next;
}

setState(State.BLOCK_INLINE_CONTENT);
// FALL-THRU
}

case BLOCK_INLINE_CONTENT: {
if (box instanceof BlockBox) {
BlockBox block = (BlockBox) box;
Box next = getNextInlineChild(block.getInlineContent());

if (next != null) {
return next;
}
}

setState(State.ILB_INLINE_CHILDREN);
// FALL-THRU
}

case ILB_INLINE_CHILDREN: {
if (box instanceof InlineLayoutBox) {
InlineLayoutBox ilb = (InlineLayoutBox) box;
Box next = getNextInlineChild(ilb.getInlineChildren());

if (next != null) {
return next;
}
}

setState(State.DONE);
// FALL-THRU
}

default:
next();
return null;
}
}

private Box getNextInlineChild(List<? extends Object> inlineChilds) {
if (inlineChilds == null) {
return null;
}

while (childIndex < inlineChilds.size()) {
Object inlineChild = inlineChilds.get(childIndex);
childIndex++;

if (inlineChild instanceof Box) {
if (hasChildren((Box) inlineChild)) {
add((Box) inlineChild);
}
return (Box) inlineChild;
}
}

return null;
}

@Override
public boolean tryAdvance(Consumer<? super Box> action) {
Box next = getNext();

if (next != null) {
action.accept(next);
return true;
} else {
while (hasNext()) {
Box next2 = getNext();
if (next2 != null) {
action.accept(next2);
return true;
}
}
}

return false;
}

@Override
public Spliterator<Box> trySplit() {
return null;
}

@Override
public long estimateSize() {
return Long.MAX_VALUE;
}

@Override
public int characteristics() {
return Spliterator.IMMUTABLE | Spliterator.NONNULL;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import com.openhtmltopdf.layout.Layer;
import com.openhtmltopdf.render.Box;
import com.openhtmltopdf.render.InlineLayoutBox;

public class LambdaUtil {
private LambdaUtil() { }
Expand All @@ -28,35 +28,25 @@ public static Stream<Box> ancestors(Box bx) {
return list.stream();
}

private static void descendants(Box bx, List<Box> out) {
if (bx != null && bx.getChildren() != null) {
out.addAll(bx.getChildren());
for (Box box : bx.getChildren()) {
descendants(box, out);

if (box instanceof InlineLayoutBox) {
List<Object> inlineChilds = ((InlineLayoutBox) box).getInlineChildren();

if (inlineChilds != null) {
for (Object obj : inlineChilds) {
if (obj instanceof Box) {
out.add((Box) obj);
descendants((Box) obj, out);
}
}
}
}
}
}
/**
* A stream of all descendant boxes not including
* InlineText or InlineBox objects.
*
* This would usually only be called after layout is concluded
* as InlineBox objects are converted to one or more InlineLayoutBox
* during layout.
*
* Should be in breadth first order.
*/
public static Stream<Box> descendants(Box parent) {
return StreamSupport.stream(new DescendantBoxSpliterator(parent), false);
}

/**
* A stream of all descendant boxes not including InlineText objects.
* See {@link #descendants(Box)}
*/
public static Stream<Box> descendants(Box bx) {
List<Box> list = new ArrayList<>();
descendants(bx, list);
return list.stream();
public static List<Box> descendantsList(Box parent) {
return descendants(parent).collect(Collectors.toList());
}

/**
Expand Down

0 comments on commit 5372cb5

Please sign in to comment.